diff --git a/.cargo/config.toml b/.cargo/config.toml index 3082e9635cf9..5b27955262ad 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,9 +3,13 @@ rustflags = [ "-Funsafe_code", "-Wclippy::as_conversions", "-Wclippy::expect_used", + "-Wclippy::index_refutable_slice", + "-Wclippy::indexing_slicing", + "-Wclippy::match_on_vec_items", "-Wclippy::missing_panics_doc", - "-Wclippy::panic_in_result_fn", + "-Wclippy::out_of_bounds_indexing", "-Wclippy::panic", + "-Wclippy::panic_in_result_fn", "-Wclippy::panicking_unwrap", "-Wclippy::todo", "-Wclippy::unimplemented", @@ -23,10 +27,7 @@ rustflags = [ [build] -rustdocflags = [ - "--cfg", - "uuid_unstable" -] +rustdocflags = ["--cfg", "uuid_unstable"] [alias] gen-pg = "generate --path ../../../../connector-template -n" diff --git a/.dockerignore b/.dockerignore index 62804a712fa1..81ef10ad2133 100644 --- a/.dockerignore +++ b/.dockerignore @@ -261,7 +261,3 @@ result* # node_modules node_modules/ - -**/connector_auth.toml -**/sample_auth.toml -**/auth.toml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 638d5540d3d6..0eb3d95bfc69 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -23,6 +23,17 @@ postman/ @juspay/hyperswitch-framework Cargo.toml @juspay/hyperswitch-framework Cargo.lock @juspay/hyperswitch-framework +crates/api_models/src/events/ @juspay/hyperswitch-analytics +crates/api_models/src/events.rs @juspay/hyperswitch-analytics +crates/api_models/src/analytics/ @juspay/hyperswitch-analytics +crates/api_models/src/analytics.rs @juspay/hyperswitch-analytics +crates/router/src/analytics.rs @juspay/hyperswitch-analytics +crates/router/src/events/ @juspay/hyperswitch-analytics +crates/router/src/events.rs @juspay/hyperswitch-analytics +crates/common_utils/src/events/ @juspay/hyperswitch-analytics +crates/common_utils/src/events.rs @juspay/hyperswitch-analytics +crates/analytics/ @juspay/hyperswitch-analytics + connector-template/ @juspay/hyperswitch-connector crates/router/src/connector/ @juspay/hyperswitch-connector crates/router/tests/connectors/ @juspay/hyperswitch-connector @@ -33,6 +44,71 @@ 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/api_models/src/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/api_models/src/user @juspay/hyperswitch-dashboard +crates/api_models/src/user.rs @juspay/hyperswitch-dashboard +crates/api_models/src/user_role.rs @juspay/hyperswitch-dashboard +crates/api_models/src/verify_connector.rs @juspay/hyperswitch-dashboard +crates/api_models/src/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/diesel_models/src/query/dashboard_metadata.rs @juspay/hyperswitch-dashboard +crates/diesel_models/src/query/user @juspay/hyperswitch-dashboard +crates/diesel_models/src/query/user_role.rs @juspay/hyperswitch-dashboard +crates/diesel_models/src/query/user.rs @juspay/hyperswitch-dashboard +crates/diesel_models/src/user @juspay/hyperswitch-dashboard +crates/diesel_models/src/user.rs @juspay/hyperswitch-dashboard +crates/diesel_models/src/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/consts/user.rs @juspay/hyperswitch-dashboard +crates/router/src/consts/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/core/connector_onboarding @juspay/hyperswitch-dashboard +crates/router/src/core/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/router/src/core/errors/user.rs @juspay/hyperswitch-dashboard +crates/router/src/core/errors/user @juspay/hyperswitch-dashboard +crates/router/src/core/user @juspay/hyperswitch-dashboard +crates/router/src/core/user.rs @juspay/hyperswitch-dashboard +crates/router/src/core/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/core/verify_connector.rs @juspay/hyperswitch-dashboard +crates/router/src/db/dashboard_metadata.rs @juspay/hyperswitch-dashboard +crates/router/src/db/user @juspay/hyperswitch-dashboard +crates/router/src/db/user.rs @juspay/hyperswitch-dashboard +crates/router/src/db/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/routes/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/router/src/routes/dummy_connector @juspay/hyperswitch-dashboard +crates/router/src/routes/dummy_connector.rs @juspay/hyperswitch-dashboard +crates/router/src/routes/user.rs @juspay/hyperswitch-dashboard +crates/router/src/routes/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/routes/verify_connector.rs @juspay/hyperswitch-dashboard +crates/router/src/services/authentication.rs @juspay/hyperswitch-dashboard +crates/router/src/services/authorization @juspay/hyperswitch-dashboard +crates/router/src/services/authorization.rs @juspay/hyperswitch-dashboard +crates/router/src/services/jwt.rs @juspay/hyperswitch-dashboard +crates/router/src/services/email/types.rs @juspay/hyperswitch-dashboard +crates/router/src/types/api/connector_onboarding @juspay/hyperswitch-dashboard +crates/router/src/types/api/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/router/src/types/api/verify_connector @juspay/hyperswitch-dashboard +crates/router/src/types/api/verify_connector.rs @juspay/hyperswitch-dashboard +crates/router/src/types/domain/user @juspay/hyperswitch-dashboard +crates/router/src/types/domain/user.rs @juspay/hyperswitch-dashboard +crates/router/src/types/storage/user.rs @juspay/hyperswitch-dashboard +crates/router/src/types/storage/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/types/storage/dashboard_metadata.rs @juspay/hyperswitch-dashboard +crates/router/src/utils/connector_onboarding @juspay/hyperswitch-dashboard +crates/router/src/utils/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/router/src/utils/user @juspay/hyperswitch-dashboard +crates/router/src/utils/user.rs @juspay/hyperswitch-dashboard +crates/router/src/utils/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/utils/verify_connector.rs @juspay/hyperswitch-dashboard + crates/router/src/scheduler/ @juspay/hyperswitch-process-tracker Dockerfile @juspay/hyperswitch-infra diff --git a/.github/git-cliff-changelog.toml b/.github/git-cliff-changelog.toml index 1d7e4080cc20..f9959eebfc69 100644 --- a/.github/git-cliff-changelog.toml +++ b/.github/git-cliff-changelog.toml @@ -14,7 +14,7 @@ body = """ {% 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") }}) + ## {{ version }} {% else -%} ## [unreleased] {% endif -%} @@ -69,7 +69,8 @@ commit_parsers = [ { 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\\(version\\)): (V|v)[\\d]+\\.[\\d]+\\.[\\d]+", skip = true }, + { message = "^(?i)(chore\\(version\\)): [0-9]{4}\\.[0-9]{2}\\.[0-9]{2}(\\.[0-9]+)?(-.+)?", skip = true }, { message = "^(?i)(chore)", group = "Miscellaneous Tasks" }, { message = "^(?i)(build)", group = "Build System / Dependencies" }, { message = "^(?i)(ci)", skip = true }, @@ -79,7 +80,7 @@ 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]*" +tag_pattern = "[0-9]{4}\\.[0-9]{2}\\.[0-9]{2}(\\.[0-9]+)?(-.+)?" # regex for skipping tags # skip_tags = "v0.1.0-beta.1" # regex for ignoring tags 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 deleted file mode 100644 index 7da9189ade58..000000000000 Binary files a/.github/secrets/connector_auth.toml.gpg and /dev/null differ diff --git a/.github/workflows/CI-pr.yml b/.github/workflows/CI-pr.yml index c79ffa63709a..d6b3d98b8c82 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 @@ -122,149 +130,53 @@ jobs: shell: bash run: sed -i 's/rustflags = \[/rustflags = \[\n "-Dwarnings",/' .cargo/config.toml - - name: Check files changed + - name: Check modified crates shell: bash run: | - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/api_models/; then - echo "api_models_changes_exist=false" >> $GITHUB_ENV - else - echo "api_models_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/cards/; then - echo "cards_changes_exist=false" >> $GITHUB_ENV - else - echo "cards_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/common_enums/; then - echo "common_enums_changes_exist=false" >> $GITHUB_ENV - else - echo "common_enums_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/common_utils/; then - echo "common_utils_changes_exist=false" >> $GITHUB_ENV - else - echo "common_utils_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/diesel_models/; then - echo "diesel_models_changes_exist=false" >> $GITHUB_ENV - else - echo "diesel_models_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/drainer/; then - echo "drainer_changes_exist=false" >> $GITHUB_ENV - else - echo "drainer_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/external_services/; then - echo "external_services_changes_exist=false" >> $GITHUB_ENV - else - echo "external_services_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/masking/; then - echo "masking_changes_exist=false" >> $GITHUB_ENV - else - echo "masking_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/redis_interface/; then - echo "redis_interface_changes_exist=false" >> $GITHUB_ENV - else - echo "redis_interface_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router/; then - echo "router_changes_exist=false" >> $GITHUB_ENV - else - echo "router_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/storage_impl/; then - echo "storage_impl_changes_exist=false" >> $GITHUB_ENV - else - echo "storage_impl_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router_derive/; then - echo "router_derive_changes_exist=false" >> $GITHUB_ENV - else - echo "router_derive_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router_env/; then - echo "router_env_changes_exist=false" >> $GITHUB_ENV - else - echo "router_env_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/test_utils/; then - echo "test_utils_changes_exist=false" >> $GITHUB_ENV - else - echo "test_utils_changes_exist=true" >> $GITHUB_ENV - fi - - - name: Cargo hack api_models - if: env.api_models_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p api_models - - - name: Cargo hack cards - if: env.cards_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p cards - - - name: Cargo hack common_enums - if: env.common_enums_changes_exist == 'true' + # Obtain a list of workspace members + workspace_members="$( + cargo metadata --format-version 1 --no-deps \ + | jq --compact-output --monochrome-output --raw-output '.workspace_members | sort | .[] | split(" ")[0]' + )" + + PACKAGES_CHECKED=() + PACKAGES_SKIPPED=() + + while IFS= read -r package_name; do + # Obtain comma-separated list of transitive workspace dependencies for each workspace member + change_paths="$(cargo tree --all-features --no-dedupe --prefix none --package "${package_name}" \ + | grep 'crates/' \ + | sort --unique \ + | awk --field-separator ' ' '{ printf "crates/%s\n", $1 }' | paste -d ',' -s -)" + + # Store change paths in an array by splitting `change_paths` by comma + IFS=',' read -ra change_paths <<< "${change_paths}" + + # A package must be checked if any of its transitive dependencies (or itself) has been modified + if git diff --exit-code --quiet "origin/${GITHUB_BASE_REF}" -- "${change_paths[@]}"; then + printf '::debug::Skipping `%s` since none of these paths were modified: %s\n' "${package_name}" "${change_paths[*]}" + PACKAGES_SKIPPED+=("${package_name}") + else + printf '::debug::Checking `%s` since at least one of these paths was modified: %s\n' "${package_name}" "${change_paths[*]}" + PACKAGES_CHECKED+=("${package_name}") + fi + done <<< "${workspace_members}" + + printf '::notice::Packages checked: %s; Packages skipped: %s\n' "${PACKAGES_CHECKED[*]}" "${PACKAGES_SKIPPED[*]}" + echo "PACKAGES_CHECKED=${PACKAGES_CHECKED[*]}" >> ${GITHUB_ENV} + echo "PACKAGES_SKIPPED=${PACKAGES_SKIPPED[*]}" >> ${GITHUB_ENV} + + - name: Run `cargo hack` on modified crates shell: bash - run: cargo hack check --each-feature --no-dev-deps -p common_enums - - - name: Cargo hack common_utils - if: env.common_utils_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p common_utils - - - name: Cargo hack diesel_models - if: env.diesel_models_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p diesel_models - - - name: Cargo hack drainer - if: env.drainer_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p drainer - - - name: Cargo hack external_services - if: env.external_services_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p external_services - - - name: Cargo hack masking - if: env.masking_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p masking - - - name: Cargo hack redis_interface - if: env.redis_interface_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p redis_interface - - - name: Cargo hack router - if: env.router_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --skip kms,basilisk,kv_store,accounts_cache,openapi --no-dev-deps -p router - - - name: Cargo hack storage_impl - if: env.storage_impl_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p storage_impl - - - name: Cargo hack router_derive - if: env.router_derive_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p router_derive - - - name: Cargo hack router_env - if: env.router_env_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p router_env + run: | + # Store packages to check in an array by splitting `PACKAGES_CHECKED` by space + IFS=' ' read -ra PACKAGES_CHECKED <<< "${PACKAGES_CHECKED}" - - name: Cargo hack test_utils - if: env.test_utils_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p test_utils + for package in "${PACKAGES_CHECKED[@]}"; do + printf '::group::Running `cargo hack` on package `%s`\n' "${package}" + cargo hack check --each-feature --all-targets --package "${package}" + echo '::endgroup::' + done # cargo-deny: # name: Run cargo-deny @@ -280,7 +192,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 +211,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 +248,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,163 +280,67 @@ 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 fi - - name: Check files changed + - name: Check modified crates shell: bash run: | - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/api_models/; then - echo "api_models_changes_exist=false" >> $GITHUB_ENV - else - echo "api_models_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/cards/; then - echo "cards_changes_exist=false" >> $GITHUB_ENV - else - echo "cards_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/common_enums/; then - echo "common_enums_changes_exist=false" >> $GITHUB_ENV - else - echo "common_enums_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/common_utils/; then - echo "common_utils_changes_exist=false" >> $GITHUB_ENV - else - echo "common_utils_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/diesel_models/; then - echo "diesel_models_changes_exist=false" >> $GITHUB_ENV - else - echo "diesel_models_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/drainer/; then - echo "drainer_changes_exist=false" >> $GITHUB_ENV - else - echo "drainer_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/external_services/; then - echo "external_services_changes_exist=false" >> $GITHUB_ENV - else - echo "external_services_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/masking/; then - echo "masking_changes_exist=false" >> $GITHUB_ENV - else - echo "masking_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/redis_interface/; then - echo "redis_interface_changes_exist=false" >> $GITHUB_ENV - else - echo "redis_interface_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router/; then - echo "router_changes_exist=false" >> $GITHUB_ENV - else - echo "router_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router_derive/; then - echo "router_derive_changes_exist=false" >> $GITHUB_ENV - else - echo "router_derive_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/storage_impl/; then - echo "storage_impl_changes_exist=false" >> $GITHUB_ENV - else - echo "storage_impl_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router_env/; then - echo "router_env_changes_exist=false" >> $GITHUB_ENV - else - echo "router_env_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/test_utils/; then - echo "test_utils_changes_exist=false" >> $GITHUB_ENV - else - echo "test_utils_changes_exist=true" >> $GITHUB_ENV - fi - - - name: Cargo hack api_models - if: env.api_models_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p api_models - - - name: Cargo hack cards - if: env.cards_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p cards - - - name: Cargo hack common_enums - if: env.common_enums_changes_exist == 'true' + # Obtain a list of workspace members + workspace_members="$( + cargo metadata --format-version 1 --no-deps \ + | jq --compact-output --monochrome-output --raw-output '.workspace_members | sort | .[] | split(" ")[0]' + )" + + PACKAGES_CHECKED=() + PACKAGES_SKIPPED=() + + while IFS= read -r package_name; do + # Obtain comma-separated list of transitive workspace dependencies for each workspace member + change_paths="$(cargo tree --all-features --no-dedupe --prefix none --package "${package_name}" \ + | grep 'crates/' \ + | sort --unique \ + | awk --field-separator ' ' '{ printf "crates/%s\n", $1 }' | paste -d ',' -s -)" + + # Store change paths in an array by splitting `change_paths` by comma + IFS=',' read -ra change_paths <<< "${change_paths}" + + # A package must be checked if any of its transitive dependencies (or itself) has been modified + if git diff --exit-code --quiet "origin/${GITHUB_BASE_REF}" -- "${change_paths[@]}"; then + printf '::debug::Skipping `%s` since none of these paths were modified: %s\n' "${package_name}" "${change_paths[*]}" + PACKAGES_SKIPPED+=("${package_name}") + else + printf '::debug::Checking `%s` since at least one of these paths was modified: %s\n' "${package_name}" "${change_paths[*]}" + PACKAGES_CHECKED+=("${package_name}") + fi + done <<< "${workspace_members}" + + printf '::notice::Packages checked: %s; Packages skipped: %s\n' "${PACKAGES_CHECKED[*]}" "${PACKAGES_SKIPPED[*]}" + echo "PACKAGES_CHECKED=${PACKAGES_CHECKED[*]}" >> ${GITHUB_ENV} + echo "PACKAGES_SKIPPED=${PACKAGES_SKIPPED[*]}" >> ${GITHUB_ENV} + + - name: Run `cargo hack` on modified crates shell: bash - run: cargo hack check --each-feature --no-dev-deps -p common_enums - - - name: Cargo hack common_utils - if: env.common_utils_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p common_utils - - - name: Cargo hack diesel_models - if: env.diesel_models_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p diesel_models - - - name: Cargo hack drainer - if: env.drainer_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p drainer - - - name: Cargo hack external_services - if: env.external_services_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p external_services - - - name: Cargo hack masking - if: env.masking_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p masking - - - name: Cargo hack redis_interface - if: env.redis_interface_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p redis_interface - - - name: Cargo hack router - if: env.router_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --skip kms,basilisk,kv_store,accounts_cache,openapi --no-dev-deps -p router - - - name: Cargo hack router_derive - if: env.router_derive_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p router_derive - - - name: Cargo hack storage_impl - if: env.storage_impl_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p storage_impl - - - name: Cargo hack router_env - if: env.router_env_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p router_env + run: | + # Store packages to check in an array by splitting `PACKAGES_CHECKED` by space + IFS=' ' read -ra PACKAGES_CHECKED <<< "${PACKAGES_CHECKED}" - - name: Cargo hack test_utils - if: env.test_utils_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p test_utils + for package in "${PACKAGES_CHECKED[@]}"; do + printf '::group::Running `cargo hack` on package `%s`\n' "${package}" + cargo hack check --each-feature --all-targets --package "${package}" + echo '::endgroup::' + done typos: name: Spell check 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..90b301bbd9e5 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 @@ -80,8 +80,8 @@ jobs: - name: Cargo hack if: ${{ github.event_name == 'push' }} shell: bash - run: cargo hack check --each-feature --no-dev-deps - + run: cargo hack check --workspace --each-feature --all-targets + - name: Cargo build release if: ${{ github.event_name == 'merge_group' }} shell: bash @@ -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' }} @@ -166,7 +166,7 @@ jobs: - name: Cargo hack if: ${{ github.event_name == 'push' }} shell: bash - run: cargo hack check --each-feature --no-dev-deps + run: cargo hack check --workspace --each-feature --all-targets - name: Cargo build release if: ${{ github.event_name == 'merge_group' }} @@ -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..f3d4635ab11d 100644 --- a/.github/workflows/connector-ui-sanity-tests.yml +++ b/.github/workflows/connector-ui-sanity-tests.yml @@ -82,24 +82,33 @@ 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 + - name: Download Encrypted TOML from S3 and Decrypt if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} env: + AWS_ACCESS_KEY_ID: ${{ secrets.CONNECTOR_CREDS_AWS_ACCESS_KEY_ID }} + AWS_REGION: ${{ secrets.CONNECTOR_CREDS_AWS_REGION }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CONNECTOR_CREDS_AWS_SECRET_ACCESS_KEY }} CONNECTOR_AUTH_PASSPHRASE: ${{ secrets.CONNECTOR_AUTH_PASSPHRASE }} + CONNECTOR_CREDS_S3_BUCKET_URI: ${{ secrets.CONNECTOR_CREDS_S3_BUCKET_URI}} + DESTINATION_FILE_NAME: "connector_auth.toml.gpg" + S3_SOURCE_FILE_NAME: "cf05a6ab-525e-4888-98b3-3b4a443b87c0.toml.gpg" shell: bash - run: ./scripts/decrypt_connector_auth.sh + run: | + mkdir -p ${HOME}/target/secrets ${HOME}/target/test + aws s3 cp "${CONNECTOR_CREDS_S3_BUCKET_URI}/${S3_SOURCE_FILE_NAME}" "${HOME}/target/secrets/${DESTINATION_FILE_NAME}" + gpg --quiet --batch --yes --decrypt --passphrase="${CONNECTOR_AUTH_PASSPHRASE}" --output "${HOME}/target/test/connector_auth.toml" "${HOME}/target/secrets/${DESTINATION_FILE_NAME}" - name: Set connector auth file path in env if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} shell: bash - run: echo "CONNECTOR_AUTH_FILE_PATH=$HOME/target/test/connector_auth.toml" >> $GITHUB_ENV + run: echo "CONNECTOR_AUTH_FILE_PATH=${HOME}/target/test/connector_auth.toml" >> $GITHUB_ENV - name: Set connector tests file path in env if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} shell: bash - run: echo "CONNECTOR_TESTS_FILE_PATH=$HOME/target/test/connector_tests.json" >> $GITHUB_ENV + run: echo "CONNECTOR_TESTS_FILE_PATH=${HOME}/target/test/connector_tests.json" >> $GITHUB_ENV - name: Set ignore_browser_profile usage in env if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} @@ -113,10 +122,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 @@ -154,9 +163,9 @@ jobs: failed_connectors=() for i in $(echo "$INPUT" | tr "," "\n"); do - echo $i + echo "${i}" if ! cargo test --package test_utils --test connectors -- "${i}_ui::" --test-threads=1; then - failed_connectors+=("$i") + failed_connectors+=("${i}") fi done diff --git a/.github/workflows/conventional-commit-check.yml b/.github/workflows/conventional-commit-check.yml deleted file mode 100644 index 5fd25e9332d1..000000000000 --- a/.github/workflows/conventional-commit-check.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Conventional Commit Message Check - -on: - # This is a dangerous event trigger as it causes the workflow to run in the - # context of the target repository. - # Avoid checking out the head of the pull request or building code from the - # pull request whenever this trigger is used. - # Since we only label pull requests, do not have a checkout step in this - # workflow, and restrict permissions on the token, this is an acceptable - # use of this trigger. - pull_request_target: - types: - - opened - - edited - - reopened - - ready_for_review - - synchronize - - merge_group: - types: - - checks_requested - -permissions: - # Reference: https://github.com/cli/cli/issues/6274 - repository-projects: read - pull-requests: write - -env: - # Allow more retries for network requests in cargo (downloading crates) and - # rustup (installing toolchains). This should help to reduce flaky CI failures - # from transient network timeouts or other issues. - CARGO_NET_RETRY: 10 - RUSTUP_MAX_RETRIES: 10 - # Use cargo's sparse index protocol - CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse - -jobs: - pr_title_check: - name: Verify PR title follows conventional commit standards - runs-on: ubuntu-latest - - steps: - - name: Install Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable 2 weeks ago - - - uses: baptiste0928/cargo-install@v2.1.0 - with: - crate: cocogitto - - - name: Verify PR title follows conventional commit standards - id: pr_title_check - if: ${{ github.event_name == 'pull_request_target' }} - shell: bash - env: - TITLE: ${{ github.event.pull_request.title }} - continue-on-error: true - run: cog verify "$TITLE" - - - name: Verify commit message follows conventional commit standards - id: commit_message_check - if: ${{ github.event_name == 'merge_group' }} - shell: bash - # Fail on error, we don't have context about PR information to update labels - continue-on-error: false - run: cog verify '${{ github.event.merge_group.head_commit.message }}' - - # GitHub CLI returns a successful error code even if the PR has the label already attached - - name: Attach 'S-conventions-not-followed' label if PR title check failed - if: ${{ github.event_name == 'pull_request_target' && steps.pr_title_check.outcome == 'failure' }} - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - gh --repo ${{ github.event.repository.full_name }} pr edit --add-label 'S-conventions-not-followed' ${{ github.event.pull_request.number }} - echo "::error::PR title does not follow conventional commit standards" - exit 1 - - # GitHub CLI returns a successful error code even if the PR does not have the label attached - - name: Remove 'S-conventions-not-followed' label if PR title check succeeded - if: ${{ github.event_name == 'pull_request_target' && steps.pr_title_check.outcome == 'success' }} - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: gh --repo ${{ github.event.repository.full_name }} pr edit --remove-label 'S-conventions-not-followed' ${{ github.event.pull_request.number }} diff --git a/.github/workflows/create-hotfix-branch.yml b/.github/workflows/create-hotfix-branch.yml index 77a8bad6bc66..d7afb388f2f8 100644 --- a/.github/workflows/create-hotfix-branch.yml +++ b/.github/workflows/create-hotfix-branch.yml @@ -8,26 +8,34 @@ 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 run: | - if [[ ${{github.ref}} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::notice::${{github.ref}} is a valid tag." + if [[ ${{github.ref}} =~ ^refs/tags/[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$ ]]; then + echo "::notice::${{github.ref}} is a CalVer tag." else - echo "::error::${{github.ref}} is not a valid tag." + echo "::error::${{github.ref}} is not a CalVer tag." exit 1 fi - name: Create hotfix branch shell: bash run: | - HOTFIX_BRANCH="hotfix-${GITHUB_REF#refs/tags/v}" + HOTFIX_BRANCH="hotfix-${GITHUB_REF#refs/tags/}" if git switch --create "$HOTFIX_BRANCH"; then git push origin "$HOTFIX_BRANCH" diff --git a/.github/workflows/create-hotfix-tag.yml b/.github/workflows/create-hotfix-tag.yml index 45699bda24dc..2250ce7ece59 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 @@ -23,10 +31,10 @@ jobs: - name: Check if the input is valid hotfix branch shell: bash run: | - if [[ ${{github.ref}} =~ ^refs/heads/hotfix-[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::notice::${{github.ref}} is a valid branch." + if [[ ${{github.ref}} =~ ^refs/heads/hotfix-[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$ ]]; then + echo "::notice::${{github.ref}} is a valid hotfix branch." else - echo "::error::${{github.ref}} is not a valid branch." + echo "::error::${{github.ref}} is not a valid hotfix branch." exit 1 fi @@ -48,11 +56,11 @@ jobs: local previous_hotfix_number local next_tag - previous_hotfix_number="$(echo "${previous_tag}" | awk -F. '{ print $4 }')" + previous_hotfix_number="$(echo "${previous_tag}" | awk -F. '{ print $4 }' | sed -E 's/([0-9]+)(-hotfix([0-9]+))?/\3/')" if [[ -z "${previous_hotfix_number}" ]]; then # Previous tag was not a hotfix tag - next_tag="${previous_tag}+hotfix.1" + next_tag="${previous_tag}-hotfix1" else # Previous tag was a hotfix tag, increment hotfix number local hotfix_number=$((previous_hotfix_number + 1)) @@ -62,7 +70,13 @@ jobs: echo "${next_tag}" } - PREVIOUS_TAG="$(git tag --merged | sort --version-sort | tail --lines 1)" + # Search for date-like tags (no strict checking), sort and obtain previous tag + PREVIOUS_TAG="$( + git tag --merged \ + | grep --extended-regexp '[0-9]{4}\.[0-9]{2}\.[0-9]{2}' \ + | sort --version-sort \ + | tail --lines 1 + )" NEXT_TAG="$(get_next_tag "${PREVIOUS_TAG}")" echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_ENV @@ -86,8 +100,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..8cbbed8187c2 100644 --- a/.github/workflows/postman-collection-runner.yml +++ b/.github/workflows/postman-collection-runner.yml @@ -50,29 +50,39 @@ jobs: steps: - name: Repository checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Decrypt connector auth file + - name: Download Encrypted TOML from S3 and Decrypt 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')}} env: + AWS_ACCESS_KEY_ID: ${{ secrets.CONNECTOR_CREDS_AWS_ACCESS_KEY_ID }} + AWS_REGION: ${{ secrets.CONNECTOR_CREDS_AWS_REGION }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CONNECTOR_CREDS_AWS_SECRET_ACCESS_KEY }} CONNECTOR_AUTH_PASSPHRASE: ${{ secrets.CONNECTOR_AUTH_PASSPHRASE }} + CONNECTOR_CREDS_S3_BUCKET_URI: ${{ secrets.CONNECTOR_CREDS_S3_BUCKET_URI}} + DESTINATION_FILE_NAME: "connector_auth.toml.gpg" + S3_SOURCE_FILE_NAME: "cf05a6ab-525e-4888-98b3-3b4a443b87c0.toml.gpg" shell: bash - run: ./scripts/decrypt_connector_auth.sh + run: | + mkdir -p ${HOME}/target/secrets ${HOME}/target/test + + aws s3 cp "${CONNECTOR_CREDS_S3_BUCKET_URI}/${S3_SOURCE_FILE_NAME}" "${HOME}/target/secrets/${DESTINATION_FILE_NAME}" + gpg --quiet --batch --yes --decrypt --passphrase="${CONNECTOR_AUTH_PASSPHRASE}" --output "${HOME}/target/test/connector_auth.toml" "${HOME}/target/secrets/${DESTINATION_FILE_NAME}" - name: Set paths in env 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')}} id: config_path shell: bash run: | - echo "CONNECTOR_AUTH_FILE_PATH=$HOME/target/test/connector_auth.toml" >> $GITHUB_ENV + echo "CONNECTOR_AUTH_FILE_PATH=${HOME}/target/test/connector_auth.toml" >> $GITHUB_ENV - name: Fetch keys 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')}} env: TOML_PATH: "./config/development.toml" run: | - LOCAL_ADMIN_API_KEY=$(yq '.secrets.admin_api_key' $TOML_PATH) - echo "ADMIN_API_KEY=$LOCAL_ADMIN_API_KEY" >> $GITHUB_ENV + LOCAL_ADMIN_API_KEY=$(yq '.secrets.admin_api_key' ${TOML_PATH}) + echo "ADMIN_API_KEY=${LOCAL_ADMIN_API_KEY}" >> $GITHUB_ENV - name: Install Rust 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 +92,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 @@ -118,7 +128,7 @@ jobs: while ! nc -z localhost 8080; do if [ $COUNT -gt 12 ]; then # Wait for up to 2 minutes (12 * 10 seconds) echo "Server did not start within a reasonable time. Exiting." - kill $SERVER_PID + kill ${SERVER_PID} exit 1 else COUNT=$((COUNT+1)) @@ -141,10 +151,10 @@ jobs: export PATH=${NEWMAN_PATH}:${PATH} failed_connectors=() - for i in $(echo "$CONNECTORS" | tr "," "\n"); do - echo $i - if ! cargo run --bin test_utils -- --connector-name="$i" --base-url="$BASE_URL" --admin-api-key="$ADMIN_API_KEY"; then - failed_connectors+=("$i") + for i in $(echo "${CONNECTORS}" | tr "," "\n"); do + echo "${i}" + if ! cargo run --bin test_utils -- --connector-name="${i}" --base-url="${BASE_URL}" --admin-api-key="${ADMIN_API_KEY}"; then + failed_connectors+=("${i}") fi done diff --git a/.github/workflows/pr-convention-checks.yml b/.github/workflows/pr-convention-checks.yml new file mode 100644 index 000000000000..37732e7c548c --- /dev/null +++ b/.github/workflows/pr-convention-checks.yml @@ -0,0 +1,128 @@ +name: Pull Request Convention Checks + +on: + # This is a dangerous event trigger as it causes the workflow to run in the + # context of the target repository. + # Avoid checking out the head of the pull request or building code from the + # pull request whenever this trigger is used. + # Since we do not have a checkout step in this workflow, this is an + # acceptable use of this trigger. + pull_request_target: + types: + - opened + - edited + - reopened + - ready_for_review + - synchronize + + merge_group: + types: + - checks_requested + +env: + # Allow more retries for network requests in cargo (downloading crates) and + # rustup (installing toolchains). This should help to reduce flaky CI failures + # from transient network timeouts or other issues. + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + +jobs: + pr_title_conventional_commit_check: + name: Verify PR title follows conventional commit standards + runs-on: ubuntu-latest + + steps: + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - uses: baptiste0928/cargo-install@v2.2.0 + with: + crate: cocogitto + + - name: Verify PR title follows conventional commit standards + if: ${{ github.event_name == 'pull_request_target' }} + shell: bash + env: + TITLE: ${{ github.event.pull_request.title }} + run: cog verify "$TITLE" + + - name: Verify commit message follows conventional commit standards + if: ${{ github.event_name == 'merge_group' }} + shell: bash + run: cog verify '${{ github.event.merge_group.head_commit.message }}' + + pr_linked_issues_check: + name: Verify PR contains one or more linked issues + runs-on: ubuntu-latest + + steps: + - name: Skip check for merge queue + if: ${{ github.event_name == 'merge_group' }} + shell: bash + run: echo "Skipping PR linked issues check for merge queue" + + - name: Generate GitHub app token + id: generate_app_token + if: ${{ github.event_name == 'pull_request_target' }} + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + owner: ${{ github.event.repository.owner.login }} + + - name: Verify PR contains one or more linked issues + if: ${{ github.event_name == 'pull_request_target' }} + shell: bash + env: + GH_TOKEN: ${{ steps.generate_app_token.outputs.token }} + run: | + # GitHub does not provide information about linked issues for a pull request via the REST API. + # This information is available only within the GraphQL API. + + # Obtain issue number and repository name with owner (in the `owner/repo` format) for all linked issues + query='query ($owner: String!, $repository: String!, $prNumber: Int!) { + repository(owner: $owner, name: $repository) { + pullRequest(number: $prNumber) { + closingIssuesReferences(first: 10) { + nodes { + number + repository { + nameWithOwner + } + } + } + } + } + }' + + # Obtain linked issues in the `owner/repo#issue_number` format, one issue per line. + # The variable contains an empty string if the pull request has no linked issues. + linked_issues="$( + gh api graphql \ + --raw-field "query=${query}" \ + --field 'owner=${{ github.event.repository.owner.login }}' \ + --field 'repository=${{ github.event.repository.name }}' \ + --field 'prNumber=${{ github.event.pull_request.number }}' \ + --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[] | "\(.repository.nameWithOwner)#\(.number)"' + )" + + if [[ -z "${linked_issues}" ]]; then + echo "::error::PR does not contain any linked issues" + exit 1 + else + echo "PR contains at least one linked issue" + fi + + while IFS= read -r issue; do + # Split `${issue}` by `#` to obtain repository with owner (in `owner/repository` format) and issue number + IFS='#' read -r repository_with_owner issue_number <<< "${issue}" + issue_state="$(gh issue view --repo "${repository_with_owner}" --json 'state' "${issue_number}" --jq '.state')" + + # Transform `${issue_state}` to lowercase for comparison + if [[ "${issue_state,,}" != 'open' ]]; then + echo "::error::At least one of the linked issues is not open" + exit 1 + fi + done <<< "${linked_issues}" 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 deleted file mode 100644 index 872c207e8aa3..000000000000 --- a/.github/workflows/release-new-version.yml +++ /dev/null @@ -1,120 +0,0 @@ -name: Release a new version - -on: - schedule: - - cron: "30 14 * * 0-4" # Run workflow at 8 PM IST every Sunday-Thursday - - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - # Allow more retries for network requests in cargo (downloading crates) and - # rustup (installing toolchains). This should help to reduce flaky CI failures - # from transient network timeouts or other issues. - CARGO_NET_RETRY: 10 - RUSTUP_MAX_RETRIES: 10 - -jobs: - create-release: - name: Release a new version - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - fetch-depth: 0 - token: ${{ secrets.AUTO_RELEASE_PAT }} - - - name: Install Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable 2 weeks ago - - - name: Install cocogitto - uses: baptiste0928/cargo-install@v2.1.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: | - git config --local user.name 'github-actions' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' - - - name: Update Postman collection files from Postman directories - shell: bash - run: | - # maybe we need to move this package.json as we need it in multiple workflows - npm ci - POSTMAN_DIR=postman/collection-dir - POSTMAN_JSON_DIR=postman/collection-json - NEWMAN_PATH=$(pwd)/node_modules/.bin - export PATH=${NEWMAN_PATH}:${PATH} - # generate postman collections for all postman directories - for connector_dir in ${POSTMAN_DIR}/* - do - connector=$(basename ${connector_dir}) - newman dir-import ${POSTMAN_DIR}/${connector} -o ${POSTMAN_JSON_DIR}/${connector}.postman_collection.json - done - - if git add postman && ! git diff --staged --quiet postman; then - git commit --message 'test(postman): update postman collection files' - echo "Changes detected and commited." - fi - - - name: Obtain previous and new tag information - shell: bash - # Only consider tags on current branch when setting PREVIOUS_TAG - run: | - 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 - NEW_TAG="$(cog bump --auto --dry-run)" - fi - echo "NEW_TAG=${NEW_TAG}" >> $GITHUB_ENV - echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_ENV - - - name: Update changelog and create tag - shell: bash - if: ${{ env.NEW_TAG != env.PREVIOUS_TAG }} - # Remove prefix 'v' from 'NEW_TAG' as cog bump --version expects only the version number - run: | - cog bump --version ${NEW_TAG#v} - - - name: Push created commit and tag - shell: bash - if: ${{ env.NEW_TAG != env.PREVIOUS_TAG }} - 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/release-nightly-version-reusable.yml b/.github/workflows/release-nightly-version-reusable.yml new file mode 100644 index 000000000000..f982a699895a --- /dev/null +++ b/.github/workflows/release-nightly-version-reusable.yml @@ -0,0 +1,158 @@ +name: Create a nightly tag + +on: + workflow_call: + secrets: + token: + description: GitHub token for authenticating with GitHub + required: true + outputs: + tag: + description: The tag that was created by the workflow + value: ${{ jobs.create-nightly-tag.outputs.tag }} + +env: + # Allow more retries for network requests in cargo (downloading crates) and + # rustup (installing toolchains). This should help to reduce flaky CI failures + # from transient network timeouts or other issues. + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + + # The branch name that this workflow is allowed to run on. + # If the workflow is run on any other branch, this workflow will fail. + ALLOWED_BRANCH_NAME: main + +jobs: + create-nightly-tag: + name: Create a nightly tag + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.token }} + + - name: Check if the workflow is run on an allowed branch + shell: bash + run: | + if [[ "${{ github.ref }}" != "refs/heads/${ALLOWED_BRANCH_NAME}" ]]; then + echo "::error::This workflow is expected to be run from the '${ALLOWED_BRANCH_NAME}' branch. Current branch: '${{ github.ref }}'" + exit 1 + fi + + - name: Check if the latest commit is a tag + shell: bash + run: | + if [[ -n "$(git tag --points-at HEAD)" ]]; then + echo "::error::The latest commit on the branch is already a tag" + exit 1 + fi + + # Pulling latest changes in case pre-release steps push new commits + - name: Pull allowed branch + shell: bash + run: git pull + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - name: Install git-cliff + uses: baptiste0928/cargo-install@v2.2.0 + with: + crate: git-cliff + version: 1.4.0 + + - name: Obtain previous and next tag information + shell: bash + run: | + # Calendar versioning format followed: `YYYY.0M.0D.MICRO` + # - MICRO version number starts from 0 (to allow for multiple tags in a single day) + # - Hotfixes or patches can be suffixed as `-hotfix1` or `-patch1` after the MICRO version number + + CURRENT_UTC_DATE="$(date --utc '+%04Y.%02m.%02d')" + + # Check if any tags exist on the current branch which contain the current UTC date + if ! git tag --merged | grep --quiet "${CURRENT_UTC_DATE}"; then + # Search for date-like tags (no strict checking), sort and obtain previous tag + PREVIOUS_TAG="$( + git tag --merged \ + | grep --extended-regexp '[0-9]{4}\.[0-9]{2}\.[0-9]{2}' \ + | sort --version-sort \ + | tail --lines 1 + )" + + # No tags with current date exist, next tag will be just tagged with current date and micro version number 0 + NEXT_MICRO_VERSION_NUMBER='0' + NEXT_TAG="${CURRENT_UTC_DATE}.${NEXT_MICRO_VERSION_NUMBER}" + + else + # Some tags exist with current date, find out latest micro version number + PREVIOUS_TAG="$( + git tag --merged \ + | grep "${CURRENT_UTC_DATE}" \ + | sort --version-sort \ + | tail --lines 1 + )" + PREVIOUS_MICRO_VERSION_NUMBER="$( + echo -n "${PREVIOUS_TAG}" \ + | sed --regexp-extended 's/[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.([0-9]+))?(-(.+))?/\2/g' + )" + # ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^ ^^^^ + # YEAR MONTH DAY MICRO Any suffix, say `hotfix1` + # + # The 2nd capture group contains the micro version number + + if [[ -z "${PREVIOUS_MICRO_VERSION_NUMBER}" ]]; then + # Micro version number is empty, set next micro version as 1 + NEXT_MICRO_VERSION_NUMBER='1' + else + # Increment previous micro version by 1 and set it as next micro version + NEXT_MICRO_VERSION_NUMBER="$((PREVIOUS_MICRO_VERSION_NUMBER + 1))" + fi + + NEXT_TAG="${CURRENT_UTC_DATE}.${NEXT_MICRO_VERSION_NUMBER}" + fi + + echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_ENV + echo "NEXT_TAG=${NEXT_TAG}" >> $GITHUB_ENV + + - name: Generate changelog + shell: bash + run: | + # Generate changelog content and store it in `release-notes.md` + git-cliff --config '.github/git-cliff-changelog.toml' --strip header --tag "${NEXT_TAG}" "${PREVIOUS_TAG}^.." \ + | sed "/## ${PREVIOUS_TAG}\$/,\$d" \ + | sed '$s/$/\n- - -/' > release-notes.md + + # Append release notes after the specified pattern in `CHANGELOG.md` + sed --in-place '0,/^- - -/!b; /^- - -/{ + a + r release-notes.md + }' CHANGELOG.md + rm release-notes.md + + - 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' + + - name: Commit, tag and push generated changelog + shell: bash + run: | + git add CHANGELOG.md + git commit --message "chore(version): ${NEXT_TAG}" + + git tag "${NEXT_TAG}" HEAD + + git push origin "${ALLOWED_BRANCH_NAME}" + git push origin "${NEXT_TAG}" + + - name: Set job outputs + shell: bash + run: | + echo "tag=${NEXT_TAG}" >> $GITHUB_OUTPUT diff --git a/.github/workflows/release-nightly-version.yml b/.github/workflows/release-nightly-version.yml new file mode 100644 index 000000000000..13e844e7c5d7 --- /dev/null +++ b/.github/workflows/release-nightly-version.yml @@ -0,0 +1,99 @@ +name: Create a nightly tag + +on: + schedule: + - cron: "0 0 * * 1-5" # Run workflow at 00:00 midnight UTC (05:30 AM IST) every Monday-Friday + + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Allow more retries for network requests in cargo (downloading crates) and + # rustup (installing toolchains). This should help to reduce flaky CI failures + # from transient network timeouts or other issues. + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + + # The branch name that this workflow is allowed to run on. + # If the workflow is run on any other branch, this workflow will fail. + ALLOWED_BRANCH_NAME: main + +jobs: + update-postman-collections: + name: Update Postman collection JSON files + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.AUTO_RELEASE_PAT }} + + - name: Check if the workflow is run on an allowed branch + shell: bash + run: | + if [[ "${{ github.ref }}" != "refs/heads/${ALLOWED_BRANCH_NAME}" ]]; then + echo "::error::This workflow is expected to be run from the '${ALLOWED_BRANCH_NAME}' branch. Current branch: '${{ github.ref }}'" + exit 1 + fi + + - name: Check if the latest commit is a tag + shell: bash + run: | + if [[ -n "$(git tag --points-at HEAD)" ]]; then + echo "::error::The latest commit on the branch is already a tag" + exit 1 + fi + + - name: Update Postman collection files from Postman directories + shell: bash + run: | + # maybe we need to move this package.json as we need it in multiple workflows + npm ci + + POSTMAN_DIR="postman/collection-dir" + POSTMAN_JSON_DIR="postman/collection-json" + NEWMAN_PATH="$(pwd)/node_modules/.bin" + export PATH="${NEWMAN_PATH}:${PATH}" + + # generate Postman collection JSON files for all Postman collection directories + for connector_dir in "${POSTMAN_DIR}"/* + do + connector="$(basename "${connector_dir}")" + newman dir-import "${POSTMAN_DIR}/${connector}" -o "${POSTMAN_JSON_DIR}/${connector}.postman_collection.json" + done + + if git add postman && ! git diff --staged --quiet postman; then + echo "POSTMAN_COLLECTION_FILES_UPDATED=true" >> $GITHUB_ENV + echo "Postman collection files have been modified" + else + echo "Postman collection files have no modifications" + fi + + - name: Set git configuration + shell: bash + if: ${{ env.POSTMAN_COLLECTION_FILES_UPDATED == 'true' }} + run: | + git config --local user.name 'github-actions' + git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + + - name: Commit and push updated Postman collections if modified + shell: bash + if: ${{ env.POSTMAN_COLLECTION_FILES_UPDATED == 'true' }} + run: | + git add postman + git commit --message 'chore(postman): update Postman collection files' + + git push origin "${ALLOWED_BRANCH_NAME}" + + create-nightly-tag: + name: Create a nightly tag + uses: ./.github/workflows/release-nightly-version-reusable.yml + needs: + - update-postman-collections + secrets: + token: ${{ secrets.AUTO_RELEASE_PAT }} diff --git a/.github/workflows/release-stable-version.yml b/.github/workflows/release-stable-version.yml new file mode 100644 index 000000000000..93bd71ef7795 --- /dev/null +++ b/.github/workflows/release-stable-version.yml @@ -0,0 +1,154 @@ +name: Release a stable version + +on: + workflow_dispatch: + inputs: + bump_type: + description: The part of the semantic version to bump. + required: true + type: choice + options: + - patch + - minor + +jobs: + create-semver-tag: + name: Create a SemVer tag + runs-on: ubuntu-latest + + steps: + - name: Generate GitHub app token + id: generate_app_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@v4 + with: + fetch-depth: 0 + + - name: Check if the input is valid CalVer tag + shell: bash + run: | + if [[ ${{github.ref}} =~ ^refs/tags/[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.([0-9]+))?(-(.+))?$ ]]; then + echo "${{github.ref}} is a valid CalVer tag." + else + echo "::error::${{github.ref}} is not a valid CalVer tag." + exit 1 + fi + + - name: Check if user is authorized to trigger workflow + shell: bash + env: + GH_TOKEN: ${{ steps.generate_app_token.outputs.token }} + run: | + echo "::add-mask::${GH_TOKEN}" + + function is_user_team_member() { + username="${1}" + team_slug="${2}" + org_name=${{ github.repository_owner }} + + # We obtain HTTP status code since the API returns: + # - 200 status code if the user is a member of the specified team + # - 404 status code if the user is not a member of the specified team + # + # We cannot use the GitHub CLI since it does not seem to provide a way to obtain + # only the HTTP status code (yet). + status_code="$( + curl \ + --location \ + --silent \ + --output /dev/null \ + --write-out '%{http_code}' \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + --header "Authorization: Bearer ${GH_TOKEN}" \ + "https://api.github.com/orgs/${org_name}/teams/${team_slug}/memberships/${username}" + )" + + # Returns a boolean value, allowing it to be directly used in if conditions + [[ status_code -eq 200 ]] + } + + allowed_teams=('hyperswitch-admins' 'hyperswitch-maintainers') + is_user_authorized=false + username=${{ github.triggering_actor }} + + for team in "${allowed_teams[@]}"; do + if is_user_team_member "${username}" "${team}"; then + is_user_authorized=true + break + fi + done + + if ${is_user_authorized}; then + echo "${username} is authorized to trigger workflow" + else + printf -v allowed_teams_comma_separated '%s, ' "${allowed_teams[@]}" + echo "::error::${username} is not authorized to trigger workflow; must be a member of one of these teams: ${allowed_teams_comma_separated%, }" + exit 1 + fi + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - name: Install git-cliff + uses: baptiste0928/cargo-install@v2.2.0 + with: + crate: git-cliff + version: 1.4.0 + + - name: Install convco + uses: baptiste0928/cargo-install@v2.2.0 + with: + crate: convco + version: 0.5.0 + + - name: Obtain previous and next tag information + shell: bash + run: | + PREVIOUS_TAG="v$(convco version --prefix 'v')" + NEXT_TAG="v$(convco version --prefix 'v' "--${{ inputs.bump_type }}")" + + echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_ENV + echo "NEXT_TAG=${NEXT_TAG}" >> $GITHUB_ENV + + # We make use of GitHub API calls to create the tag to have signed tags + - name: Create SemVer tag + shell: bash + env: + GH_TOKEN: ${{ steps.generate_app_token.outputs.token }} + run: | + # Create a lightweight tag to point to the checked out CalVer tag + gh api \ + --method POST \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + '/repos/{owner}/{repo}/git/refs' \ + --raw-field "ref=refs/tags/${NEXT_TAG}" \ + --raw-field 'sha=${{ github.sha }}' + + - name: Generate changelog + shell: bash + run: | + # Override git-cliff tag pattern to only consider SemVer tags + export GIT_CLIFF__GIT__TAG_PATTERN='v[0-9]*' + + # Update heading format in git-cliff changelog template to include date + sed -i 's/## {{ version }}/## {{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }})/' .github/git-cliff-changelog.toml + + # Generate changelog content and store it in `release-notes.md` + git-cliff --config '.github/git-cliff-changelog.toml' --strip header --tag "${NEXT_TAG}" "${PREVIOUS_TAG}^.." \ + | sed "/## ${PREVIOUS_TAG}\$/,\$d" > release-notes.md + + - name: Upload changelog as build artifact + uses: actions/upload-artifact@v4 + with: + name: release-notes.md + path: release-notes.md + if-no-files-found: error diff --git a/.github/workflows/validate-openapi-spec.yml b/.github/workflows/validate-openapi-spec.yml index 530c59c9236d..210f82064832 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 }} @@ -44,7 +52,7 @@ jobs: - name: Generate the OpenAPI spec file shell: bash - run: cargo run --features openapi -- generate-openapi-spec + run: cargo run -p openapi - name: Install `swagger-cli` shell: bash @@ -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/.gitignore b/.gitignore index 62804a712fa1..81ef10ad2133 100644 --- a/.gitignore +++ b/.gitignore @@ -261,7 +261,3 @@ result* # node_modules node_modules/ - -**/connector_auth.toml -**/sample_auth.toml -**/auth.toml diff --git a/.typos.toml b/.typos.toml index 1ac38a005c9e..40acb1305892 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 @@ -35,6 +36,7 @@ ba = "ba" # ignore minor commit conversions ede = "ede" # ignore minor commit conversions daa = "daa" # Commit id afe = "afe" # Commit id +Hashi = "Hashi" # HashiCorp [files] extend-exclude = [ diff --git a/CHANGELOG.md b/CHANGELOG.md index 427fa7403e4c..117d4cd90e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,1211 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.02.01.0 + +### Features + +- **dashboard_metadata:** Add email alert for Prod Intent ([#3482](https://github.com/juspay/hyperswitch/pull/3482)) ([`94cd7b6`](https://github.com/juspay/hyperswitch/commit/94cd7b689758a71e13a3eaa655335e658d13afc8)) +- **pm_list:** Add required fields for google pay ([#3196](https://github.com/juspay/hyperswitch/pull/3196)) ([`7f2c434`](https://github.com/juspay/hyperswitch/commit/7f2c434bd29d337dadde8b71a9137797f1c03ec0)) + +### Bug Fixes + +- **configs:** Add configs for Payme 3DS ([#3415](https://github.com/juspay/hyperswitch/pull/3415)) ([`58771b8`](https://github.com/juspay/hyperswitch/commit/58771b8985a53c83185805f770fee26c5836c645)) + +### Refactors + +- **connector:** + - [NMI] change error message from not supported to not implemented ([#2848](https://github.com/juspay/hyperswitch/pull/2848)) ([`7575341`](https://github.com/juspay/hyperswitch/commit/757534104ee0411a887c993e45cc1fb883e82992)) + - [Paypal] Change error message from NotSupported to NotImplemented ([#2877](https://github.com/juspay/hyperswitch/pull/2877)) ([`7251f64`](https://github.com/juspay/hyperswitch/commit/7251f6474fdac3575202971e55638c435ca5c4c8)) + - [Adyen] change expiresAt time from string to unixtimestamp ([#3506](https://github.com/juspay/hyperswitch/pull/3506)) ([`b7c0f9a`](https://github.com/juspay/hyperswitch/commit/b7c0f9aa098c880314a529bc10015256ce2139f7)) + +### Miscellaneous Tasks + +- **connector_events_fields:** Added refund_id, dispute_id to connector events ([#3424](https://github.com/juspay/hyperswitch/pull/3424)) ([`90a2462`](https://github.com/juspay/hyperswitch/commit/90a24625ce312e4e7681cf4cc470e6365a052f8a)) + +**Full Changelog:** [`2024.01.31.1...2024.02.01.0`](https://github.com/juspay/hyperswitch/compare/2024.01.31.1...2024.02.01.0) + +- - - + +## 2024.01.31.1 + +### Features + +- **users:** + - Added blacklist for users ([#3469](https://github.com/juspay/hyperswitch/pull/3469)) ([`e331d2d`](https://github.com/juspay/hyperswitch/commit/e331d2d5569405b89052c6bb59f7e755523f6f15)) + - Add `merchant_id` in `EmailToken` and change user status in reset password ([#3473](https://github.com/juspay/hyperswitch/pull/3473)) ([`db3d53f`](https://github.com/juspay/hyperswitch/commit/db3d53ff1d8b42d107fafe7a6efe7ec9f155d5a0)) +- Add deep health check for analytics ([#3438](https://github.com/juspay/hyperswitch/pull/3438)) ([`7597f3b`](https://github.com/juspay/hyperswitch/commit/7597f3b692124a762c3b212b604938be2d64175a)) + +### Bug Fixes + +- **connector:** [Trustpay] add merchant_id in gpay session response for trustpay ([#3471](https://github.com/juspay/hyperswitch/pull/3471)) ([`20568dc`](https://github.com/juspay/hyperswitch/commit/20568dc976687b8b2bfba12ab2db8926cf1c14ed)) + +### Miscellaneous Tasks + +- **postman:** Update Postman collection files ([`a4b9782`](https://github.com/juspay/hyperswitch/commit/a4b97828be103d601a5007f8e4274837faa6886f)) + +**Full Changelog:** [`2024.01.31.0...2024.01.31.1`](https://github.com/juspay/hyperswitch/compare/2024.01.31.0...2024.01.31.1) + +- - - + +## 2024.01.31.0 + +### Features + +- **connector:** [noon] add revoke mandate ([#3487](https://github.com/juspay/hyperswitch/pull/3487)) ([`b5bc8c4`](https://github.com/juspay/hyperswitch/commit/b5bc8c4e7cfdde8251ed0e2e3835ed5e3f1435c4)) + +### Bug Fixes + +- **connector:** [BOA/Cybersource] Handle Invalid Api Secret ([#3485](https://github.com/juspay/hyperswitch/pull/3485)) ([`224c1cf`](https://github.com/juspay/hyperswitch/commit/224c1cf2a421441433097618cc1dd3db224d5915)) +- **user:** Change permission for sample data ([#3462](https://github.com/juspay/hyperswitch/pull/3462)) ([`610c1c5`](https://github.com/juspay/hyperswitch/commit/610c1c575253ddf7a1a31ef941efaae2dd676b48)) + +### Refactors + +- **core:** Restrict requires_customer_action in confirm ([#3235](https://github.com/juspay/hyperswitch/pull/3235)) ([`d2accde`](https://github.com/juspay/hyperswitch/commit/d2accdef410319733d6174057bdca468bde1ae83)) + +### Miscellaneous Tasks + +- **config:** [ADYEN] Add configs for PIX in WASM ([#3498](https://github.com/juspay/hyperswitch/pull/3498)) ([`9821935`](https://github.com/juspay/hyperswitch/commit/9821935933e178765b3b0d0bcbfdf4ab041c3bc2)) + +**Full Changelog:** [`2024.01.30.1...2024.01.31.0`](https://github.com/juspay/hyperswitch/compare/2024.01.30.1...2024.01.31.0) + +- - - + +## 2024.01.30.1 + +### Features + +- **config:** Add iDEAL and Sofort Env Configs ([#3492](https://github.com/juspay/hyperswitch/pull/3492)) ([`46c1822`](https://github.com/juspay/hyperswitch/commit/46c1822d0e367e59420c9d087428bc3b12794445)) +- **connector:** + - [Bluesnap] Metadata to connector metadata mapping ([#3331](https://github.com/juspay/hyperswitch/pull/3331)) ([`b2afdc3`](https://github.com/juspay/hyperswitch/commit/b2afdc35465426bd11428d8d4ac743617a443128)) + - [Stripe] Metadata to connector metadata mapping ([#3295](https://github.com/juspay/hyperswitch/pull/3295)) ([`864a8d7`](https://github.com/juspay/hyperswitch/commit/864a8d7b02acda5ea593cae83594962ea249c16d)) +- **core:** Update card_details for an existing mandate ([#3452](https://github.com/juspay/hyperswitch/pull/3452)) ([`02074df`](https://github.com/juspay/hyperswitch/commit/02074dfc23f1a126e76935ba5311c6aed6590ca5)) +- **pm_list:** Add required fields for sofort ([#3192](https://github.com/juspay/hyperswitch/pull/3192)) ([`3d55e3b`](https://github.com/juspay/hyperswitch/commit/3d55e3ba45619978e8ca9e5012c156dc017d2879)) +- **users:** Signin and Verify Email changes for User Invitation changes ([#3420](https://github.com/juspay/hyperswitch/pull/3420)) ([`d91da89`](https://github.com/juspay/hyperswitch/commit/d91da89065a6870f05e1ff9db007d16a58454c84)) + +### Bug Fixes + +- **logging:** Add flow to persistent logs fields ([#3472](https://github.com/juspay/hyperswitch/pull/3472)) ([`ac49103`](https://github.com/juspay/hyperswitch/commit/ac491038b16c77fc7f2249042b35dfb1d58e653d)) +- Empty payment attempts on payment retrieve ([#3447](https://github.com/juspay/hyperswitch/pull/3447)) ([`bec4f2a`](https://github.com/juspay/hyperswitch/commit/bec4f2a24e2236f7814119a6ebf0363cbf598540)) + +### Refactors + +- **payment_link:** Segregated payment link in html css js files, sdk over flow issue, surcharge bug, block SPM customer call for payment link ([#3410](https://github.com/juspay/hyperswitch/pull/3410)) ([`a7bc8c6`](https://github.com/juspay/hyperswitch/commit/a7bc8c655f5b745dccd4d818ac3ceb08c3b80c0e)) +- **settings:** Make the function to deserialize hashsets more generic ([#3104](https://github.com/juspay/hyperswitch/pull/3104)) ([`87191d6`](https://github.com/juspay/hyperswitch/commit/87191d687cd66bf096bfb98ffe51a805b4b76a03)) +- Add support for extending file storage to other schemes and provide a runtime flag for the same ([#3348](https://github.com/juspay/hyperswitch/pull/3348)) ([`a9638d1`](https://github.com/juspay/hyperswitch/commit/a9638d118e0b68653fef3bec2ce8aa3c47feedd3)) + +### Miscellaneous Tasks + +- **analytics:** + - Adding status code to connector Kafka events ([#3393](https://github.com/juspay/hyperswitch/pull/3393)) ([`d6807ab`](https://github.com/juspay/hyperswitch/commit/d6807abba46136eabadcbfbc51bce421144dca2c)) + - Adding dispute id to api log events ([#3450](https://github.com/juspay/hyperswitch/pull/3450)) ([`937aea9`](https://github.com/juspay/hyperswitch/commit/937aea906e759e6e8a76a424db99ed052d46b7d2)) +- **kv:** Add metrics while pushing to stream ([#3364](https://github.com/juspay/hyperswitch/pull/3364)) ([`8c0c49c`](https://github.com/juspay/hyperswitch/commit/8c0c49c6bb02d4ec58242bc90eadfb267c24481e)) + +**Full Changelog:** [`2024.01.30.0...2024.01.30.1`](https://github.com/juspay/hyperswitch/compare/2024.01.30.0...2024.01.30.1) + +- - - + +## 2024.01.30.0 + +### Features + +- **router:** Add request_details logger middleware for 400 bad requests ([#3414](https://github.com/juspay/hyperswitch/pull/3414)) ([`dd0d2dc`](https://github.com/juspay/hyperswitch/commit/dd0d2dc2dd9a6263bbb8a99d1f0b2077f38dd621)) + +### Refactors + +- **openapi:** Move openapi to separate crate to decrease compile times ([#3110](https://github.com/juspay/hyperswitch/pull/3110)) ([`7d8d68f`](https://github.com/juspay/hyperswitch/commit/7d8d68faba55dfcb2886c63ae7969ebd4b9ec98c)) + +### Miscellaneous Tasks + +- **configs:** [NMI] add wasm changes for prod dashboard ([#3470](https://github.com/juspay/hyperswitch/pull/3470)) ([`3fbffdc`](https://github.com/juspay/hyperswitch/commit/3fbffdc242dafe7983c542573b7c6362f99331e6)) + +**Full Changelog:** [`2024.01.29.0...2024.01.30.0`](https://github.com/juspay/hyperswitch/compare/2024.01.29.0...2024.01.30.0) + +- - - + +## 2024.01.29.0 + +### Features + +- **connector:** [Adyen] Add support for PIX Payment Method ([#3236](https://github.com/juspay/hyperswitch/pull/3236)) ([`fc6e68f`](https://github.com/juspay/hyperswitch/commit/fc6e68f7f07bf2d48466fa493596c0db02d7550a)) +- **core:** + - [CYBERSOURCE] Add original authorized amount in router data ([#3417](https://github.com/juspay/hyperswitch/pull/3417)) ([`47fbe48`](https://github.com/juspay/hyperswitch/commit/47fbe486cec252b8befca38f1b7ea77cc0823ee5)) + - Add outgoing webhook for manual `partial_capture` events ([#3388](https://github.com/juspay/hyperswitch/pull/3388)) ([`d5e9866`](https://github.com/juspay/hyperswitch/commit/d5e9866b522bad3e62f6f6c0d7993f5dcc2939af)) +- **logging:** Add a logging middleware to log all api requests ([#3437](https://github.com/juspay/hyperswitch/pull/3437)) ([`c2946cf`](https://github.com/juspay/hyperswitch/commit/c2946cfe05ffa81a66643e04eff5e89b545d2d43)) +- **user:** + - Add support to delete user ([#3374](https://github.com/juspay/hyperswitch/pull/3374)) ([`7777710`](https://github.com/juspay/hyperswitch/commit/777771048a8144aac9e2f837c85531e139ecc125)) + - Support multiple invites ([#3422](https://github.com/juspay/hyperswitch/pull/3422)) ([`a59ac7d`](https://github.com/juspay/hyperswitch/commit/a59ac7d5b98f27f5fb34206c20ef9c37a07259a3)) + +### Bug Fixes + +- **connector:** + - Use `ConnectorError::InvalidConnectorConfig` for an invalid `CoinbaseConnectorMeta` ([#3168](https://github.com/juspay/hyperswitch/pull/3168)) ([`d827c9a`](https://github.com/juspay/hyperswitch/commit/d827c9af29b8516f379e648e00f4ab307ae1a34d)) + - Fix connector template script ([#3453](https://github.com/juspay/hyperswitch/pull/3453)) ([`9a54838`](https://github.com/juspay/hyperswitch/commit/9a54838b0529013ab8f449ec6b347a104b55f8f7)) + - [HELCIM] Handle 4XX Errors ([#3458](https://github.com/juspay/hyperswitch/pull/3458)) ([`ec859ea`](https://github.com/juspay/hyperswitch/commit/ec859eabbfb8a511f0fffd30a47a144fb07f2886)) +- **core:** Return surcharge in payment method list response if passed in create request ([#3363](https://github.com/juspay/hyperswitch/pull/3363)) ([`3507ad6`](https://github.com/juspay/hyperswitch/commit/3507ad60b2f1fd84d32eb4d97fe0a847db6f2045)) +- **euclid_wasm:** Include `payouts` feature in `default` features ([#3392](https://github.com/juspay/hyperswitch/pull/3392)) ([`b45e4ca`](https://github.com/juspay/hyperswitch/commit/b45e4ca2a3788823701bdeac2e2a8c1147bb071a)) + +### Refactors + +- **connector:** + - [Iatapay] refactor authorize flow and fix payment status mapping ([#2409](https://github.com/juspay/hyperswitch/pull/2409)) ([`f0c7bb9`](https://github.com/juspay/hyperswitch/commit/f0c7bb9a5228f2ee31858fea07abe4ecee9b78a2)) + - Use utility function to raise payment method not implemented errors ([#1871](https://github.com/juspay/hyperswitch/pull/1871)) ([`66cd5b2`](https://github.com/juspay/hyperswitch/commit/66cd5b2fc9a32085608ed34e0af477dcafe4b957)) +- **payouts:** Propagate `Not Implemented` error ([#3429](https://github.com/juspay/hyperswitch/pull/3429)) ([`5ab4437`](https://github.com/juspay/hyperswitch/commit/5ab44377b84941b8b59f9e73b1d1f0c3889eb02b)) + +### Miscellaneous Tasks + +- **configs:** [Cashtocode] wasm changes for CAD, CHF currency ([#3461](https://github.com/juspay/hyperswitch/pull/3461)) ([`10055c1`](https://github.com/juspay/hyperswitch/commit/10055c1a7354faae8d0f504e0851d2046df5734a)) + +**Full Changelog:** [`2024.01.25.0...2024.01.29.0`](https://github.com/juspay/hyperswitch/compare/2024.01.25.0...2024.01.29.0) + +- - - + +## 2024.01.25.0 + +### Refactors + +- **configs:** Add configs for deployments to environments ([#3265](https://github.com/juspay/hyperswitch/pull/3265)) ([`77c1bbb`](https://github.com/juspay/hyperswitch/commit/77c1bbb5a3fe3244cd988ac1260a4a31ae7fcd20)) + +**Full Changelog:** [`2024.01.24.1...2024.01.25.0`](https://github.com/juspay/hyperswitch/compare/2024.01.24.1...2024.01.25.0) + +- - - + +## 2024.01.24.1 + +### Features + +- **hashicorp:** Implement hashicorp secrets manager solution ([#3297](https://github.com/juspay/hyperswitch/pull/3297)) ([`629d546`](https://github.com/juspay/hyperswitch/commit/629d546aa7c774e86d609abec3b3ab5cf0d100a7)) + +### Refactors + +- **Router:** [Noon] revert adding new field max_amount to mandate request ([#3435](https://github.com/juspay/hyperswitch/pull/3435)) ([`4cd65a2`](https://github.com/juspay/hyperswitch/commit/4cd65a24f70fdef160eb2d87654f1e30538c3339)) +- **compatibility:** Revert add multiuse mandates support in stripe compatibility ([#3436](https://github.com/juspay/hyperswitch/pull/3436)) ([`8a019f0`](https://github.com/juspay/hyperswitch/commit/8a019f08acf74e04c3ae9c8790dd481301bdcfee)) + +### Miscellaneous Tasks + +- **ckh-source:** Updated ckh analytics source tables ([#3397](https://github.com/juspay/hyperswitch/pull/3397)) ([`3f343d3`](https://github.com/juspay/hyperswitch/commit/3f343d36bff7ce8f73602a2391d205367d5581c7)) + +**Full Changelog:** [`2024.01.24.0...2024.01.24.1`](https://github.com/juspay/hyperswitch/compare/2024.01.24.0...2024.01.24.1) + +- - - + +## 2024.01.24.0 + +### Miscellaneous Tasks + +- **postman:** Update Postman collection files ([`7885b2a`](https://github.com/juspay/hyperswitch/commit/7885b2a213f474da3e018ddeb56bc6e407c48471)) + +**Full Changelog:** [`2024.01.23.0...2024.01.24.0`](https://github.com/juspay/hyperswitch/compare/2024.01.23.0...2024.01.24.0) + +- - - + +## 2024.01.23.0 + +### Features + +- **compatibility:** Add multiuse mandates support in stripe compatibility ([#3425](https://github.com/juspay/hyperswitch/pull/3425)) ([`4a8104e`](https://github.com/juspay/hyperswitch/commit/4a8104e5f8dd2cfd03de4055baf1256cb7533895)) + +**Full Changelog:** [`2024.01.22.1...2024.01.23.0`](https://github.com/juspay/hyperswitch/compare/2024.01.22.1...2024.01.23.0) + +- - - + +## 2024.01.22.1 + +### Features + +- **core:** Send `customer_name` to connectors when creating customer ([#3380](https://github.com/juspay/hyperswitch/pull/3380)) ([`7813cee`](https://github.com/juspay/hyperswitch/commit/7813ceece2081b73f1374e2ee5a9a673f0b72127)) + +### Miscellaneous Tasks + +- Chore(deps): bump the cargo group across 1 directories with 3 updates ([#3409](https://github.com/juspay/hyperswitch/pull/3409)) ([`6c46e9c`](https://github.com/juspay/hyperswitch/commit/6c46e9c19b304bb11f304e60c46e8abf67accf6d)) + +**Full Changelog:** [`2024.01.22.0...2024.01.22.1`](https://github.com/juspay/hyperswitch/compare/2024.01.22.0...2024.01.22.1) + +- - - + +## 2024.01.22.0 + +### Features + +- **user_roles:** Add accept invitation API and `UserJWTAuth` ([#3365](https://github.com/juspay/hyperswitch/pull/3365)) ([`a47372a`](https://github.com/juspay/hyperswitch/commit/a47372a451b60defda35fa212565b889ed5b2d2b)) + +### Documentation + +- Add link to api docs ([#3405](https://github.com/juspay/hyperswitch/pull/3405)) ([`4e1e78e`](https://github.com/juspay/hyperswitch/commit/4e1e78ecd962f4b34fa04f611f03e8e6f6e1bd7c)) + +**Full Changelog:** [`2024.01.19.1...2024.01.22.0`](https://github.com/juspay/hyperswitch/compare/2024.01.19.1...2024.01.22.0) + +- - - + +## 2024.01.19.1 + +### Bug Fixes + +- **connector:** [CRYPTOPAY] Fix header generation for PSYNC ([#3402](https://github.com/juspay/hyperswitch/pull/3402)) ([`ec16ed0`](https://github.com/juspay/hyperswitch/commit/ec16ed0f82f258c5699d54a386f67aff06c0d144)) +- **frm:** Update FRM manual review flow ([#3176](https://github.com/juspay/hyperswitch/pull/3176)) ([`5255ba9`](https://github.com/juspay/hyperswitch/commit/5255ba9170c633899cd4c3bbe24a44b429546f15)) + +### Refactors + +- Rename `s3` feature flag to `aws_s3` ([#3341](https://github.com/juspay/hyperswitch/pull/3341)) ([`1c04ac7`](https://github.com/juspay/hyperswitch/commit/1c04ac751240f5c931df0f282af1e0ad745e9509)) + +**Full Changelog:** [`2024.01.19.0...2024.01.19.1`](https://github.com/juspay/hyperswitch/compare/2024.01.19.0...2024.01.19.1) + +- - - + +## 2024.01.19.0 + +### Features + +- **users:** + - Add `preferred_merchant_id` column and update user details API ([#3373](https://github.com/juspay/hyperswitch/pull/3373)) ([`862a1b5`](https://github.com/juspay/hyperswitch/commit/862a1b5303ff304cca41d3553f652fd1091aab9b)) + - Added get role from jwt api ([#3385](https://github.com/juspay/hyperswitch/pull/3385)) ([`7516a16`](https://github.com/juspay/hyperswitch/commit/7516a16763877c03ecc35fda19388bbd021c5cc7)) + +### Refactors + +- **recon:** Update recipient email and mail body for ProFeatureRequest ([#3381](https://github.com/juspay/hyperswitch/pull/3381)) ([`5a791aa`](https://github.com/juspay/hyperswitch/commit/5a791aaf4dc05e8ffdb60464a03b6fc41f860581)) + +**Full Changelog:** [`2024.01.18.1...2024.01.19.0`](https://github.com/juspay/hyperswitch/compare/2024.01.18.1...2024.01.19.0) + +- - - + +## 2024.01.18.1 + +### Bug Fixes + +- **connector:** + - Trustpay zen error mapping ([#3255](https://github.com/juspay/hyperswitch/pull/3255)) ([`e816ccf`](https://github.com/juspay/hyperswitch/commit/e816ccfbdd7b0e24464aa93421e399d63f23b17c)) + - [Cashtocode] update amount from i64 to f64 in webhook payload ([#3382](https://github.com/juspay/hyperswitch/pull/3382)) ([`059e866`](https://github.com/juspay/hyperswitch/commit/059e86607dc271c25bb3d23f5adfc7d5f21f62fb)) +- **metrics:** Add TASKS_ADDED_COUNT and TASKS_RESET_COUNT metrics in router scheduler flow ([#3189](https://github.com/juspay/hyperswitch/pull/3189)) ([`b4df40d`](https://github.com/juspay/hyperswitch/commit/b4df40db25f6ea743c7a25db47e8f1d8e0d544e3)) +- **user:** Fetch profile_id for sample data ([#3358](https://github.com/juspay/hyperswitch/pull/3358)) ([`2f693ad`](https://github.com/juspay/hyperswitch/commit/2f693ad1fd857280ef30c6cc0297fb926f0e79e8)) + +### Refactors + +- **connector:** [Volt] Refactor Payments and Refunds Webhooks ([#3377](https://github.com/juspay/hyperswitch/pull/3377)) ([`acb3296`](https://github.com/juspay/hyperswitch/commit/acb329672297cd7337d0b0239e4c662257812e8a)) +- **core:** Add locker config to enable or disable locker ([#3352](https://github.com/juspay/hyperswitch/pull/3352)) ([`bd5356e`](https://github.com/juspay/hyperswitch/commit/bd5356e7e7cf61f9d07fe9b67c9c5bb38fddf9c7)) + +**Full Changelog:** [`2024.01.18.0...2024.01.18.1`](https://github.com/juspay/hyperswitch/compare/2024.01.18.0...2024.01.18.1) + +- - - + +## 2024.01.18.0 + +### Features + +- **connector_events:** Added api to fetch connector event logs ([#3319](https://github.com/juspay/hyperswitch/pull/3319)) ([`68a3a28`](https://github.com/juspay/hyperswitch/commit/68a3a280676c8309f9becffae545b134b5e1f2ea)) +- **payment_method:** Add capability to store bank details using /payment_methods endpoint ([#3113](https://github.com/juspay/hyperswitch/pull/3113)) ([`01c2de2`](https://github.com/juspay/hyperswitch/commit/01c2de223f60595d77c06a59a40dfe041e02cfee)) + +### Bug Fixes + +- **core:** Add validation for authtype and metadata in update payment connector ([#3305](https://github.com/juspay/hyperswitch/pull/3305)) ([`52f38d3`](https://github.com/juspay/hyperswitch/commit/52f38d3d5a7d035e8211e1f51c8f982232e2d7ab)) +- **events:** Fix event generation for paymentmethods list ([#3337](https://github.com/juspay/hyperswitch/pull/3337)) ([`ac8d81b`](https://github.com/juspay/hyperswitch/commit/ac8d81b32b3d91b875113d32782a8c62e39ba2a8)) + +### Refactors + +- **connector:** [cybersource] recurring mandate flow ([#3354](https://github.com/juspay/hyperswitch/pull/3354)) ([`387c1c4`](https://github.com/juspay/hyperswitch/commit/387c1c491bdc413ae361d04f0be25eaa58e72fa9)) +- [Noon] adding new field max_amount to mandate request ([#3209](https://github.com/juspay/hyperswitch/pull/3209)) ([`eb2a61d`](https://github.com/juspay/hyperswitch/commit/eb2a61d8597995838f21b8233653c691118b2191)) + +### Miscellaneous Tasks + +- **router:** Remove recon from default features ([#3370](https://github.com/juspay/hyperswitch/pull/3370)) ([`928beec`](https://github.com/juspay/hyperswitch/commit/928beecdd7fe9e09b38ffe750627ca4af94ffc93)) + +**Full Changelog:** [`2024.01.17.0...2024.01.18.0`](https://github.com/juspay/hyperswitch/compare/2024.01.17.0...2024.01.18.0) + +- - - + +## 2024.01.17.0 + +### Features + +- **connector:** [BANKOFAMERICA] Implement 3DS flow for cards ([#3343](https://github.com/juspay/hyperswitch/pull/3343)) ([`d533c98`](https://github.com/juspay/hyperswitch/commit/d533c98b5107fb6876c11b183eb9bc382a77a2f1)) +- **recon:** Add recon APIs ([#3345](https://github.com/juspay/hyperswitch/pull/3345)) ([`8678f8d`](https://github.com/juspay/hyperswitch/commit/8678f8d1448b5ce430931bfbbc269ef979d9eea7)) + +### Bug Fixes + +- **connector_onboarding:** Check if connector exists for the merchant account and add reset tracking id API ([#3229](https://github.com/juspay/hyperswitch/pull/3229)) ([`58cc8d6`](https://github.com/juspay/hyperswitch/commit/58cc8d6109ce49d385b06c762ab3f6670f5094eb)) +- **payment_link:** Added expires_on in payment response ([#3332](https://github.com/juspay/hyperswitch/pull/3332)) ([`5ad3f89`](https://github.com/juspay/hyperswitch/commit/5ad3f8939afafce3eec39704dcaa92270b384dcd)) + +**Full Changelog:** [`2024.01.12.1...2024.01.17.0`](https://github.com/juspay/hyperswitch/compare/2024.01.12.1...2024.01.17.0) + +- - - + +## 2024.01.12.1 + +### Miscellaneous Tasks + +- **config:** Add merchant_secret config for webhooks for cashtocode and volt in wasm dashboard ([#3333](https://github.com/juspay/hyperswitch/pull/3333)) ([`57f2cff`](https://github.com/juspay/hyperswitch/commit/57f2cff75e58b0a7811492a1fdb636f59dcefbd0)) +- Add api reference for blocklist ([#3336](https://github.com/juspay/hyperswitch/pull/3336)) ([`f381d86`](https://github.com/juspay/hyperswitch/commit/f381d86b7c9fa79d632991c74cab53d0181231c6)) + +**Full Changelog:** [`2024.01.12.0...2024.01.12.1`](https://github.com/juspay/hyperswitch/compare/2024.01.12.0...2024.01.12.1) + +- - - + +## 2024.01.12.0 + +### Features + +- **connector:** + - [BOA/Cyb] Include merchant metadata in capture and void requests ([#3308](https://github.com/juspay/hyperswitch/pull/3308)) ([`5a5400c`](https://github.com/juspay/hyperswitch/commit/5a5400cf5b539996b2f327c51d4a07b4a86fd1be)) + - [Volt] Add support for refund webhooks ([#3326](https://github.com/juspay/hyperswitch/pull/3326)) ([`e376f68`](https://github.com/juspay/hyperswitch/commit/e376f68c167a289957a4372df108797088ab1f6e)) + - [BOA/CYB] Store AVS response in connector_metadata ([#3271](https://github.com/juspay/hyperswitch/pull/3271)) ([`e75b11e`](https://github.com/juspay/hyperswitch/commit/e75b11e98ac4c8d37c842c8ee0ccf361dcb52793)) +- **euclid_wasm:** Config changes for NMI ([#3329](https://github.com/juspay/hyperswitch/pull/3329)) ([`ed07c5b`](https://github.com/juspay/hyperswitch/commit/ed07c5ba90868a3132ca90d72219db3ba8978232)) +- **outgoingwebhookevent:** Adding api for query to fetch outgoing webhook events log ([#3310](https://github.com/juspay/hyperswitch/pull/3310)) ([`54d44be`](https://github.com/juspay/hyperswitch/commit/54d44bef730c0679f3535f66e89e88139d70ba2e)) +- **payment_link:** Added sdk layout option payment link ([#3207](https://github.com/juspay/hyperswitch/pull/3207)) ([`6117652`](https://github.com/juspay/hyperswitch/commit/61176524ca0c11c605538a1da9a267837193e1ec)) +- **router:** Payment_method block ([#3056](https://github.com/juspay/hyperswitch/pull/3056)) ([`bb09613`](https://github.com/juspay/hyperswitch/commit/bb096138b5937092badd02741fb869ee35e2e3cc)) +- **users:** Invite user without email ([#3328](https://github.com/juspay/hyperswitch/pull/3328)) ([`6a47063`](https://github.com/juspay/hyperswitch/commit/6a4706323c61f3722dc543993c55084dc9ff9850)) +- Feat(connector): [cybersource] Implement 3DS flow for cards ([#3290](https://github.com/juspay/hyperswitch/pull/3290)) ([`6fb3b00`](https://github.com/juspay/hyperswitch/commit/6fb3b00e82d1e3c03dc1c816ffa6353cc7991a53)) +- Add support for card extended bin in payment attempt ([#3312](https://github.com/juspay/hyperswitch/pull/3312)) ([`cc3eefd`](https://github.com/juspay/hyperswitch/commit/cc3eefd317117d761cdcc76804f3510952d4cec2)) + +### Bug Fixes + +- **core:** Surcharge with saved card failure ([#3318](https://github.com/juspay/hyperswitch/pull/3318)) ([`5a1a3da`](https://github.com/juspay/hyperswitch/commit/5a1a3da7502ce9e13546b896477d82719162d5b6)) +- **refund:** Add merchant_connector_id in refund ([#3303](https://github.com/juspay/hyperswitch/pull/3303)) ([`af43b07`](https://github.com/juspay/hyperswitch/commit/af43b07e4394458db478bc16e5fb8d3b0d636a31)) +- **router:** Add config to avoid connector tokenization for `apple pay` `simplified flow` ([#3234](https://github.com/juspay/hyperswitch/pull/3234)) ([`4f9c04b`](https://github.com/juspay/hyperswitch/commit/4f9c04b856761b9c0486abad4c36de191da2c460)) +- Update amount_capturable based on intent_status and payment flow ([#3278](https://github.com/juspay/hyperswitch/pull/3278)) ([`469ea20`](https://github.com/juspay/hyperswitch/commit/469ea20214aa7c1a3b4b86520724c2509ae37b0b)) + +### Refactors + +- **router:** + - Flagged order_details validation to skip validation ([#3116](https://github.com/juspay/hyperswitch/pull/3116)) ([`8626bda`](https://github.com/juspay/hyperswitch/commit/8626bda6d5aa9e7531edc7ea50ed4f30c3b7227a)) + - Restricted list payment method Customer to api-key based ([#3100](https://github.com/juspay/hyperswitch/pull/3100)) ([`9eaebe8`](https://github.com/juspay/hyperswitch/commit/9eaebe8db3d83105ef1e8fc784241e1fb795dd22)) + +### Miscellaneous Tasks + +- Remove connector auth TOML files from `.gitignore` and `.dockerignore` ([#3330](https://github.com/juspay/hyperswitch/pull/3330)) ([`9f6ef3f`](https://github.com/juspay/hyperswitch/commit/9f6ef3f2240052053b5b7df0a13a5503d8141d56)) + +**Full Changelog:** [`2024.01.11.0...2024.01.12.0`](https://github.com/juspay/hyperswitch/compare/2024.01.11.0...2024.01.12.0) + +- - - + +## 2024.01.11.0 + +### Features + +- **core:** Add new payments webhook events ([#3212](https://github.com/juspay/hyperswitch/pull/3212)) ([`e0e28b8`](https://github.com/juspay/hyperswitch/commit/e0e28b87c0647252918ef110cd7614c46b5cf943)) +- **payment_link:** Add status page for payment link ([#3213](https://github.com/juspay/hyperswitch/pull/3213)) ([`50e4d79`](https://github.com/juspay/hyperswitch/commit/50e4d797da31b570b5920b33d77c24a21d9871e2)) + +### Bug Fixes + +- **euclid_wasm:** Update braintree config prod ([#3288](https://github.com/juspay/hyperswitch/pull/3288)) ([`8830563`](https://github.com/juspay/hyperswitch/commit/8830563748ed20c40b7a21a66e9ad9fd02ddcf0e)) + +### Refactors + +- **connector:** [bluesnap] add connector_txn_id fallback for webhook ([#3315](https://github.com/juspay/hyperswitch/pull/3315)) ([`a69e876`](https://github.com/juspay/hyperswitch/commit/a69e876f8212cb94202686e073005c23b1b2fc35)) +- Removed basilisk feature ([#3281](https://github.com/juspay/hyperswitch/pull/3281)) ([`612f8d9`](https://github.com/juspay/hyperswitch/commit/612f8d9d5f5bcba78aa64c3128cc72be0f2860ea)) + +### Miscellaneous Tasks + +- Nits and small code improvements found during investigation of PR#3168 ([#3259](https://github.com/juspay/hyperswitch/pull/3259)) ([`fe3cf54`](https://github.com/juspay/hyperswitch/commit/fe3cf54781302c733c1682ded2c1735544407a5f)) + +**Full Changelog:** [`2024.01.10.0...2024.01.11.0`](https://github.com/juspay/hyperswitch/compare/2024.01.10.0...2024.01.11.0) + +- - - + +## 2024.01.10.0 + +### Features + +- **Connector:** [VOLT] Add support for Payments Webhooks ([#3155](https://github.com/juspay/hyperswitch/pull/3155)) ([`eba7896`](https://github.com/juspay/hyperswitch/commit/eba789640b72cdfbc17d0994d16ce111a1788fe5)) +- **pm_list:** Add required fields for Ideal ([#3183](https://github.com/juspay/hyperswitch/pull/3183)) ([`1c3c5f6`](https://github.com/juspay/hyperswitch/commit/1c3c5f6b0cff9a0037175ba92c002cdf4249108d)) + +### Bug Fixes + +- **connector:** + - [BOA/CYB] Fix Metadata Error ([#3283](https://github.com/juspay/hyperswitch/pull/3283)) ([`71044a1`](https://github.com/juspay/hyperswitch/commit/71044a14ed87ac0cd7d2bb2009f0e59c79bd344c)) + - [BOA, Cybersource] capture error_code ([#3239](https://github.com/juspay/hyperswitch/pull/3239)) ([`ecf51b5`](https://github.com/juspay/hyperswitch/commit/ecf51b5e3a30f055634edfafcd36f64cef535a53)) +- **outgoingwebhookevents:** Throw an error when outgoing webhook events env var not found ([#3291](https://github.com/juspay/hyperswitch/pull/3291)) ([`ee044a0`](https://github.com/juspay/hyperswitch/commit/ee044a0be811a53842c69f64c27d9995d84b7040)) +- **users:** Added merchant name is list merchants ([#3289](https://github.com/juspay/hyperswitch/pull/3289)) ([`8a354f4`](https://github.com/juspay/hyperswitch/commit/8a354f42295a3167d0e846c9522bc091ebdca3f4)) +- **wasm:** Fix failing `wasm-pack build` for `euclid_wasm` ([#3284](https://github.com/juspay/hyperswitch/pull/3284)) ([`5eb6711`](https://github.com/juspay/hyperswitch/commit/5eb67114646674fe227f073e417f26beb97e9a43)) + +### Refactors + +- Pass customer object to `make_pm_data` ([#3246](https://github.com/juspay/hyperswitch/pull/3246)) ([`36c32c3`](https://github.com/juspay/hyperswitch/commit/36c32c377ae788c96b578303eae5d029e3044b7c)) + +### Miscellaneous Tasks + +- **postman:** Update Postman collection files ([`8fc68ad`](https://github.com/juspay/hyperswitch/commit/8fc68adc7fb6a23d4a2970a05f5739db6010a53d)) + +**Full Changelog:** [`2024.01.08.0...2024.01.10.0`](https://github.com/juspay/hyperswitch/compare/2024.01.08.0...2024.01.10.0) + +- - - + +## 2024.01.08.0 + +### Features + +- **analytics:** Adding outgoing webhooks kafka event ([#3140](https://github.com/juspay/hyperswitch/pull/3140)) ([`1d26df2`](https://github.com/juspay/hyperswitch/commit/1d26df28bc5e1db359272b40adae70bfba9b7360)) +- **connector:** Add Revoke mandate flow ([#3261](https://github.com/juspay/hyperswitch/pull/3261)) ([`90ac26a`](https://github.com/juspay/hyperswitch/commit/90ac26a92f837568be5181108fdb1272171bbf23)) +- **merchant_account:** Add list multiple merchants in `MerchantAccountInterface` ([#3220](https://github.com/juspay/hyperswitch/pull/3220)) ([`c3172ef`](https://github.com/juspay/hyperswitch/commit/c3172ef60603325a1d9e5cab45e72d23a383e218)) +- **payments:** Add payment id in all the payment logs ([#3142](https://github.com/juspay/hyperswitch/pull/3142)) ([`7766245`](https://github.com/juspay/hyperswitch/commit/7766245478f72b0bc942922b1138c87a239be153)) +- **pm_list:** Add required fields for eps ([#3169](https://github.com/juspay/hyperswitch/pull/3169)) ([`bfd8a5a`](https://github.com/juspay/hyperswitch/commit/bfd8a5a31abb3c95cc9ca21689d5c30a6dc4ce8d)) +- Add deep health check ([#3210](https://github.com/juspay/hyperswitch/pull/3210)) ([`f30ba89`](https://github.com/juspay/hyperswitch/commit/f30ba89884d3abf2356cf1870d833a97d2411f69)) +- Include version number in response headers and on application startup ([#3045](https://github.com/juspay/hyperswitch/pull/3045)) ([`252443a`](https://github.com/juspay/hyperswitch/commit/252443a50dc48939eb08b3bcd67273bb71bbe349)) + +### Bug Fixes + +- **analytics:** + - Fixed response code to 501 ([#3119](https://github.com/juspay/hyperswitch/pull/3119)) ([`00008c1`](https://github.com/juspay/hyperswitch/commit/00008c16c1c20f1f34381d0fc7e55ef05183e776)) + - Added response to the connector outgoing event ([#3129](https://github.com/juspay/hyperswitch/pull/3129)) ([`d152c3a`](https://github.com/juspay/hyperswitch/commit/d152c3a1ca70c39f5c64edf63b5995f6cf02c88a)) +- **connector:** + - [NMI] Populating `ErrorResponse` with required fields and Mapping `connector_response_reference_id` ([#3214](https://github.com/juspay/hyperswitch/pull/3214)) ([`64babd3`](https://github.com/juspay/hyperswitch/commit/64babd34786ba8e6f63aa1dba1cbd1bc6264f2ac)) + - [Stripe] Deserialization Error while parsing Dispute Webhook Body ([#3256](https://github.com/juspay/hyperswitch/pull/3256)) ([`01b4ac3`](https://github.com/juspay/hyperswitch/commit/01b4ac30e40a55b05fe3585d0544b21125762bc7)) +- **router:** + - Multiple incremental_authorizations with kv enabled ([#3185](https://github.com/juspay/hyperswitch/pull/3185)) ([`f78d02d`](https://github.com/juspay/hyperswitch/commit/f78d02d981dd7b35f2150f204b327847b811badd)) + - Payment link api contract change ([#2975](https://github.com/juspay/hyperswitch/pull/2975)) ([`3cd7496`](https://github.com/juspay/hyperswitch/commit/3cd74966b279dc1c43935dc1bceb1c69b9eb0643)) +- **user:** Add integration_completed enum in metadata type ([#3245](https://github.com/juspay/hyperswitch/pull/3245)) ([`3ab71fb`](https://github.com/juspay/hyperswitch/commit/3ab71fbd5ac86f12cf19d17561e428d33c51a4cf)) +- **users:** Fix wrong redirection url in magic link ([#3217](https://github.com/juspay/hyperswitch/pull/3217)) ([`000e644`](https://github.com/juspay/hyperswitch/commit/000e64438838461ea930545405fb2ee0d3c4356c)) +- Introduce net_amount field in payment response ([#3115](https://github.com/juspay/hyperswitch/pull/3115)) ([`23e0c63`](https://github.com/juspay/hyperswitch/commit/23e0c6354185d666771c07b8534e42380cc50812)) + +### Refactors + +- **api_lock:** Allow api lock on psync only when force sync is true ([#3242](https://github.com/juspay/hyperswitch/pull/3242)) ([`ac5349c`](https://github.com/juspay/hyperswitch/commit/ac5349cd7160f67f7a56f48f54981cf3dc1e5b52)) +- **drainer:** Change logic for trimming the stream and refactor for modularity ([#3128](https://github.com/juspay/hyperswitch/pull/3128)) ([`de7a607`](https://github.com/juspay/hyperswitch/commit/de7a607e66847ff4bbddcbbafa50d54a56f02f62)) +- **euclid_wasm:** Update wasm config ([#3222](https://github.com/juspay/hyperswitch/pull/3222)) ([`7ea50c3`](https://github.com/juspay/hyperswitch/commit/7ea50c3a78bc1a091077c23999a69dda1cf0f463)) +- Address panics due to indexing and slicing ([#3233](https://github.com/juspay/hyperswitch/pull/3233)) ([`34318bc`](https://github.com/juspay/hyperswitch/commit/34318bc1f12a1298e8993021a2d516cf86049980)) + +### Miscellaneous Tasks + +- Address Rust 1.75 clippy lints ([#3231](https://github.com/juspay/hyperswitch/pull/3231)) ([`c8279b1`](https://github.com/juspay/hyperswitch/commit/c8279b110e6c55784f042aebb956931e1870b0ca)) + +**Full Changelog:** [`v1.106.1...2024.01.08.0`](https://github.com/juspay/hyperswitch/compare/v1.106.1...2024.01.08.0) + +- - - + +## 1.106.1 (2024-01-05) + +### Bug Fixes + +- **connector:** [iatapay] change refund amount ([#3244](https://github.com/juspay/hyperswitch/pull/3244)) ([`e79604b`](https://github.com/juspay/hyperswitch/commit/e79604bd4681a69802f3c3169dd94424e3688e42)) + +**Full Changelog:** [`v1.106.0...v1.106.1`](https://github.com/juspay/hyperswitch/compare/v1.106.0...v1.106.1) + +- - - + + +## 1.106.0 (2024-01-04) + +### Features + +- **connector:** + - [BOA] Populate merchant_defined_information with metadata ([#3208](https://github.com/juspay/hyperswitch/pull/3208)) ([`18eca7e`](https://github.com/juspay/hyperswitch/commit/18eca7e9fbe6cdc101bd135c4618882b7a5455bf)) + - [CYBERSOURCE] Refactor cybersource ([#3215](https://github.com/juspay/hyperswitch/pull/3215)) ([`e06ba14`](https://github.com/juspay/hyperswitch/commit/e06ba148b666772fe79d7050d0c505dd2f04f87c)) +- **customers:** Add JWT Authentication for `/customers` APIs ([#3179](https://github.com/juspay/hyperswitch/pull/3179)) ([`aefe618`](https://github.com/juspay/hyperswitch/commit/aefe6184ec3e3156877c72988ca0f92454a47e7d)) + +### Bug Fixes + +- **connector:** [Volt] Error handling for auth response ([#3187](https://github.com/juspay/hyperswitch/pull/3187)) ([`a51c54d`](https://github.com/juspay/hyperswitch/commit/a51c54d39d3687c6a06176895435ac66fa194d7b)) +- **core:** Fix recurring mandates flow for cyber source ([#3224](https://github.com/juspay/hyperswitch/pull/3224)) ([`6a1743e`](https://github.com/juspay/hyperswitch/commit/6a1743ebe993d5abb53f2ce1b8b383aa4a9553fb)) +- **middleware:** Add support for logging request-id sent in request ([#3225](https://github.com/juspay/hyperswitch/pull/3225)) ([`0f72b55`](https://github.com/juspay/hyperswitch/commit/0f72b5527aab221b8e69e737e5d19abdd0696150)) + +### Refactors + +- **connector:** [NMI] Include mandatory fields for card 3DS ([#3203](https://github.com/juspay/hyperswitch/pull/3203)) ([`a46b8a7`](https://github.com/juspay/hyperswitch/commit/a46b8a7b05367fbbdbf4fca89d8a6b29110a4e1c)) + +### Testing + +- **postman:** Update postman collection files ([`0248d35`](https://github.com/juspay/hyperswitch/commit/0248d35dd49d2dc7e5e4da6b60a3ee3577c8eac9)) + +### Miscellaneous Tasks + +- Fix channel handling for consumer workflow loop ([#3223](https://github.com/juspay/hyperswitch/pull/3223)) ([`51e1fac`](https://github.com/juspay/hyperswitch/commit/51e1fac556fdd8775e0bbc858b0b3cc50a7e88ec)) + +**Full Changelog:** [`v1.105.0...v1.106.0`](https://github.com/juspay/hyperswitch/compare/v1.105.0...v1.106.0) + +- - - + + +## 1.105.0 (2023-12-23) + +### Features + +- **connector:** [BOA/CYBERSOURCE] Populate connector_transaction_id ([#3202](https://github.com/juspay/hyperswitch/pull/3202)) ([`110d3d2`](https://github.com/juspay/hyperswitch/commit/110d3d211be2edf47533cc5297ae159cad0e5034)) + +**Full Changelog:** [`v1.104.0...v1.105.0`](https://github.com/juspay/hyperswitch/compare/v1.104.0...v1.105.0) + +- - - + + +## 1.104.0 (2023-12-22) + +### Features + +- **connector:** [BOA] Implement apple pay manual flow ([#3191](https://github.com/juspay/hyperswitch/pull/3191)) ([`25fd3d5`](https://github.com/juspay/hyperswitch/commit/25fd3d502e48f10dd3acbdc88caea4007310d4ee)) +- **router:** Make the billing country for apple pay as optional field ([#3188](https://github.com/juspay/hyperswitch/pull/3188)) ([`15987cc`](https://github.com/juspay/hyperswitch/commit/15987cc81ecba3c1d0de4fa0a12424066a8842eb)) + +### Bug Fixes + +- **connector:** + - [Trustpay] Use `connector_request_reference_id` for merchant reference instead of `payment_id` ([#2885](https://github.com/juspay/hyperswitch/pull/2885)) ([`c51c761`](https://github.com/juspay/hyperswitch/commit/c51c761677e8c5ff80de40f8796f340cf1331f96)) + - [BOA/Cyb] Truncate state length to <20 ([#3198](https://github.com/juspay/hyperswitch/pull/3198)) ([`79a18e2`](https://github.com/juspay/hyperswitch/commit/79a18e2bf7bb1f338cf982fb1a152add2ed4e087)) + - [Iatapay] fix error response handling when payment is failed ([#3197](https://github.com/juspay/hyperswitch/pull/3197)) ([`716a74c`](https://github.com/juspay/hyperswitch/commit/716a74cf8449583541c426a5c427c9e32f5b2528)) + - [BOA] Display 2XX Failure Errors ([#3200](https://github.com/juspay/hyperswitch/pull/3200)) ([`07fd9be`](https://github.com/juspay/hyperswitch/commit/07fd9bedf02a1d70fc248fbbab480a5e24a7f077)) + - [CYBERSOURCE] Display 2XX Failure Errors ([#3201](https://github.com/juspay/hyperswitch/pull/3201)) ([`86c2622`](https://github.com/juspay/hyperswitch/commit/86c26221357e14b585f44c6ebe46962c085f6552)) +- **users:** Wrong `user_role` insertion in `invite_user` for new users ([#3193](https://github.com/juspay/hyperswitch/pull/3193)) ([`b06a8d6`](https://github.com/juspay/hyperswitch/commit/b06a8d6e0d7fc4fb1bec30f702d64f0bd5e1068e)) + +**Full Changelog:** [`v1.103.1...v1.104.0`](https://github.com/juspay/hyperswitch/compare/v1.103.1...v1.104.0) + +- - - + + +## 1.103.1 (2023-12-21) + +### Bug Fixes + +- **connector:** + - Remove set_body method for connectors implementing default get_request_body ([#3182](https://github.com/juspay/hyperswitch/pull/3182)) ([`a5e141b`](https://github.com/juspay/hyperswitch/commit/a5e141b542622e7065f0e0070a3cddacde78fd8a)) + - [Paypal] remove shipping address as mandatory field for paypal wallet ([#3181](https://github.com/juspay/hyperswitch/pull/3181)) ([`680ed60`](https://github.com/juspay/hyperswitch/commit/680ed603c5113ec29fbd13c4c633e18ad4ad10ee)) + +**Full Changelog:** [`v1.103.0...v1.103.1`](https://github.com/juspay/hyperswitch/compare/v1.103.0...v1.103.1) + +- - - + + +## 1.103.0 (2023-12-20) + +### Features + +- **connector:** + - [NMI] Implement webhook for Payments and Refunds ([#3164](https://github.com/juspay/hyperswitch/pull/3164)) ([`30c1401`](https://github.com/juspay/hyperswitch/commit/30c14019d067ad5f105563f205eb1941010233e8)) + - [BOA] Handle BOA 5XX errors ([#3178](https://github.com/juspay/hyperswitch/pull/3178)) ([`1d80949`](https://github.com/juspay/hyperswitch/commit/1d80949bef1228bf432dc445eaba15afccb030bd)) +- **connector-config:** Add wasm support for dashboard connector configuration ([#3138](https://github.com/juspay/hyperswitch/pull/3138)) ([`b0ffbe9`](https://github.com/juspay/hyperswitch/commit/b0ffbe9355b7e38226994c1ccbbe80cdbc77adde)) +- **db:** Implement `AuthorizationInterface` for `MockDb` ([#3151](https://github.com/juspay/hyperswitch/pull/3151)) ([`396a64f`](https://github.com/juspay/hyperswitch/commit/396a64f3bbad6e75d4b263286a7ef6a2f09b180e)) +- **postman:** [Prophetpay] Add test cases ([#2946](https://github.com/juspay/hyperswitch/pull/2946)) ([`583d7b8`](https://github.com/juspay/hyperswitch/commit/583d7b87a711102e4e62417f3191ac837886eca9)) + +### Bug Fixes + +- **connector:** + - [NMI] Fix response deserialization for vault id creation ([#3166](https://github.com/juspay/hyperswitch/pull/3166)) ([`d44daaf`](https://github.com/juspay/hyperswitch/commit/d44daaf539021a9cbc33c9391172c38825d74dcd)) + - Connector wise validation for zero auth flow ([#3159](https://github.com/juspay/hyperswitch/pull/3159)) ([`45ba128`](https://github.com/juspay/hyperswitch/commit/45ba128b6ab39f513dd114567d9915acf0eaea20)) +- **events:** Add logger for incoming webhook payload ([#3171](https://github.com/juspay/hyperswitch/pull/3171)) ([`cf47a65`](https://github.com/juspay/hyperswitch/commit/cf47a65916fd4fb5c996946ffd579fd6755d02f7)) +- **users:** Send correct `user_role` values in `switch_merchant` response ([#3167](https://github.com/juspay/hyperswitch/pull/3167)) ([`dc589d5`](https://github.com/juspay/hyperswitch/commit/dc589d580f1382874bc755d3719bd3244fdedc67)) + +### Refactors + +- **core:** Fix payment status for 4xx ([#3177](https://github.com/juspay/hyperswitch/pull/3177)) ([`e7949c2`](https://github.com/juspay/hyperswitch/commit/e7949c23b9be56a4cd763d4990c1a95c0fefae95)) +- **payment_methods:** Make the card_holder_name as an empty string if not sent ([#3173](https://github.com/juspay/hyperswitch/pull/3173)) ([`b98e53d`](https://github.com/juspay/hyperswitch/commit/b98e53d5cba5a5af04ada9bd83fa7bd2e27462d9)) + +### Testing + +- **postman:** Update postman collection files ([`6890e90`](https://github.com/juspay/hyperswitch/commit/6890e9029d90bfd518ba23979a0bd507853dc983)) + +### Documentation + +- **connector:** Update connector integration documentation ([#3041](https://github.com/juspay/hyperswitch/pull/3041)) ([`ce5514e`](https://github.com/juspay/hyperswitch/commit/ce5514eadfce240bc4cefb472405f37432a8507b)) + +**Full Changelog:** [`v1.102.1...v1.103.0`](https://github.com/juspay/hyperswitch/compare/v1.102.1...v1.103.0) + +- - - + + +## 1.102.1 (2023-12-18) + +### Bug Fixes + +- **connector:** [BOA/CYBERSOURCE] Update error handling ([#3156](https://github.com/juspay/hyperswitch/pull/3156)) ([`8e484dd`](https://github.com/juspay/hyperswitch/commit/8e484ddab8d3f4463299c7f7e8ce75b8dd628599)) +- **euclid_wasm:** Add function to retrieve keys for 3ds and surcharge decision manager ([#3160](https://github.com/juspay/hyperswitch/pull/3160)) ([`30fe9d1`](https://github.com/juspay/hyperswitch/commit/30fe9d19e4955035a370f8f9ce37963cdb76c68a)) +- **payment_link:** Added amount conversion to base unit based on currency ([#3162](https://github.com/juspay/hyperswitch/pull/3162)) ([`0fa61a9`](https://github.com/juspay/hyperswitch/commit/0fa61a9dd194c5b3688f8f68b056c263d92327d0)) +- Change prodintent name in dashboard metadata ([#3161](https://github.com/juspay/hyperswitch/pull/3161)) ([`8db3361`](https://github.com/juspay/hyperswitch/commit/8db3361d80f674a28a3916830a4b0c1c2b89776a)) + +### Refactors + +- **connector:** + - [Helcim] change error message from not supported to not implemented ([#2850](https://github.com/juspay/hyperswitch/pull/2850)) ([`41b5a82`](https://github.com/juspay/hyperswitch/commit/41b5a82bafa9b0392bb43ed268fefc5187b48636)) + - [Forte] change error message from not supported to not implemented ([#2847](https://github.com/juspay/hyperswitch/pull/2847)) ([`3fc0e2d`](https://github.com/juspay/hyperswitch/commit/3fc0e2d8195948d50f735df5192ae0f8431b432b)) + - [Cryptopay] change error message from not supported to not implemented ([#2846](https://github.com/juspay/hyperswitch/pull/2846)) ([`2d895be`](https://github.com/juspay/hyperswitch/commit/2d895be9856d17cd923665568aa9b6e54fc1a305)) +- **router:** [ACI] change payment error message from not supported to not implemented error ([#2837](https://github.com/juspay/hyperswitch/pull/2837)) ([`cc12e8a`](https://github.com/juspay/hyperswitch/commit/cc12e8a2435e5e47eeec77c620c747b156a3e16b)) +- **users:** Rename `user_roles` and `dashboard_metadata` columns ([#3135](https://github.com/juspay/hyperswitch/pull/3135)) ([`e3589e6`](https://github.com/juspay/hyperswitch/commit/e3589e641c8a0b3b690b82f09a61d512db2d9932)) + +**Full Changelog:** [`v1.102.0+hotfix.1...v1.102.1`](https://github.com/juspay/hyperswitch/compare/v1.102.0+hotfix.1...v1.102.1) + +- - - + + +## 1.102.0 (2023-12-17) + +### Features + +- **connector:** + - [CYBERSOURCE] Implement Google Pay ([#3139](https://github.com/juspay/hyperswitch/pull/3139)) ([`4ae6af4`](https://github.com/juspay/hyperswitch/commit/4ae6af4632bbef5d21c3cb28538dcc4a94a10789)) + - [PlaceToPay] Implement Cards for PlaceToPay ([#3117](https://github.com/juspay/hyperswitch/pull/3117)) ([`107c66f`](https://github.com/juspay/hyperswitch/commit/107c66fec331376aa8c9f1e710e1503793fde119)) + - [CYBERSOURCE] Implement Apple Pay ([#3149](https://github.com/juspay/hyperswitch/pull/3149)) ([`5f53d84`](https://github.com/juspay/hyperswitch/commit/5f53d84a8b92f8aab67d09666b45362b287809ff)) + - [NMI] Implement 3DS for Cards ([#3143](https://github.com/juspay/hyperswitch/pull/3143)) ([`7df4523`](https://github.com/juspay/hyperswitch/commit/7df45235b1b55c3e4f1205169fb512d2aadc98ac)) + +### Bug Fixes + +- **connector:** + - [Checkout] Fix status mapping for checkout ([#3073](https://github.com/juspay/hyperswitch/pull/3073)) ([`5b2c329`](https://github.com/juspay/hyperswitch/commit/5b2c3291d4fbe3c4154c187b4e915dc3365e761a)) + - [Cybersource] signature authentication in incremental_authorization flow ([#3141](https://github.com/juspay/hyperswitch/pull/3141)) ([`d47a7cc`](https://github.com/juspay/hyperswitch/commit/d47a7cc418b0f4bb609d99f4a463a14c39df46e4)) +- [CYBERSOURCE] Fix Status Mapping ([#3144](https://github.com/juspay/hyperswitch/pull/3144)) ([`62c0c47`](https://github.com/juspay/hyperswitch/commit/62c0c47e99f154399687a32caf9999b365da60ae)) + +### Testing + +- **postman:** Update postman collection files ([`d40de4c`](https://github.com/juspay/hyperswitch/commit/d40de4c8b51010a9e6a3164196702a20c2ab3563)) + +### Miscellaneous Tasks + +- **deps:** Bump zerocopy from 0.7.26 to 0.7.31 ([#3136](https://github.com/juspay/hyperswitch/pull/3136)) ([`d8de3c2`](https://github.com/juspay/hyperswitch/commit/d8de3c285c90103da93f0f3fd0241924dabd256f)) +- **events:** Remove duplicate logs ([#3148](https://github.com/juspay/hyperswitch/pull/3148)) ([`a78fed7`](https://github.com/juspay/hyperswitch/commit/a78fed73babace05b4f668ef219909277045ba85)) + +**Full Changelog:** [`v1.101.0...v1.102.0`](https://github.com/juspay/hyperswitch/compare/v1.101.0...v1.102.0) + +- - - + + +## 1.101.0 (2023-12-14) + +### Features + +- **payments:** Add outgoing payments webhooks ([#3133](https://github.com/juspay/hyperswitch/pull/3133)) ([`f457846`](https://github.com/juspay/hyperswitch/commit/f4578463d5e1a0f442aacebdfa7af0460489ba8c)) + +### Bug Fixes + +- **connector:** [CashToCode]Fix cashtocode redirection for evoucher pm type ([#3131](https://github.com/juspay/hyperswitch/pull/3131)) ([`71a86a8`](https://github.com/juspay/hyperswitch/commit/71a86a804e15e4d053f92cfddb36a15cf7b77f7a)) +- **locker:** Fix double serialization for json request ([#3134](https://github.com/juspay/hyperswitch/pull/3134)) ([`70b86b7`](https://github.com/juspay/hyperswitch/commit/70b86b71e4809d2a47c6bc1214f72c37d3325c37)) +- **router:** Add routing cache invalidation on payment connector update ([#3132](https://github.com/juspay/hyperswitch/pull/3132)) ([`1f84865`](https://github.com/juspay/hyperswitch/commit/1f848659f135542fdfa967b3b48ad6cdf69fda2c)) + +**Full Changelog:** [`v1.100.0...v1.101.0`](https://github.com/juspay/hyperswitch/compare/v1.100.0...v1.101.0) + +- - - + + +## 1.100.0 (2023-12-14) + +### Features + +- **connector:** + - [RISKIFIED] Add support for riskified frm connector ([#2533](https://github.com/juspay/hyperswitch/pull/2533)) ([`151a30f`](https://github.com/juspay/hyperswitch/commit/151a30f4eed10924cd93bf7f4f66976af0ab8314)) + - [HELCIM] Add connector_request_reference_id in invoice_number ([#3087](https://github.com/juspay/hyperswitch/pull/3087)) ([`3cc9642`](https://github.com/juspay/hyperswitch/commit/3cc9642f3ac4c07fb675e9ff4032832819d877a1)) +- **core:** Enable surcharge support for all connectors ([#3109](https://github.com/juspay/hyperswitch/pull/3109)) ([`57e1ae9`](https://github.com/juspay/hyperswitch/commit/57e1ae9dea6ff70fb1bca47c479c35026c167bad)) +- **events:** Add type info to outgoing requests & maintain structural & PII type info ([#2956](https://github.com/juspay/hyperswitch/pull/2956)) ([`6e82b0b`](https://github.com/juspay/hyperswitch/commit/6e82b0bd746b405281f79b86a3cd92b550a33f68)) +- **external_services:** Adds encrypt function for KMS ([#3111](https://github.com/juspay/hyperswitch/pull/3111)) ([`bca7cdb`](https://github.com/juspay/hyperswitch/commit/bca7cdb4c14b5fbb40d8cbf59fd1756ad27ac674)) + +### Bug Fixes + +- **api_locking:** Fix the unit interpretation for `LockSettings` expiry ([#3121](https://github.com/juspay/hyperswitch/pull/3121)) ([`3f4167d`](https://github.com/juspay/hyperswitch/commit/3f4167dbd477c793e1a4cc572da0c12d66f2b649)) +- **connector:** [trustpay] make paymentId optional field ([#3101](https://github.com/juspay/hyperswitch/pull/3101)) ([`62a7c30`](https://github.com/juspay/hyperswitch/commit/62a7c3053c5e276091f5bd54a5679caef58a4ace)) +- **docker-compose:** Remove label list from docker compose yml ([#3118](https://github.com/juspay/hyperswitch/pull/3118)) ([`e1e23fd`](https://github.com/juspay/hyperswitch/commit/e1e23fd987cae96e56311d1cfdcb225d9327860c)) +- Validate refund amount with amount_captured instead of amount ([#3120](https://github.com/juspay/hyperswitch/pull/3120)) ([`be13d15`](https://github.com/juspay/hyperswitch/commit/be13d15d3c0214c863e131cf1dbe184d5baec5d7)) + +### Refactors + +- **connector:** [Wise] Error Message For Connector Implementation ([#2952](https://github.com/juspay/hyperswitch/pull/2952)) ([`1add2c0`](https://github.com/juspay/hyperswitch/commit/1add2c059f4fb5653f33e2f3ce454793caf2d595)) +- **payments:** Add support for receiving card_holder_name field as an empty string ([#3127](https://github.com/juspay/hyperswitch/pull/3127)) ([`4d19d8b`](https://github.com/juspay/hyperswitch/commit/4d19d8b1d18f49f02e951c5025d35cf5d62cec1b)) + +### Testing + +- **postman:** Update postman collection files ([`a5618cd`](https://github.com/juspay/hyperswitch/commit/a5618cd5d6eb5b007f7927f05e777e875195a678)) + +**Full Changelog:** [`v1.99.0...v1.100.0`](https://github.com/juspay/hyperswitch/compare/v1.99.0...v1.100.0) + +- - - + + +## 1.99.0 (2023-12-12) + +### Features + +- **connector:** [Placetopay] Add Connector Template Code ([#3084](https://github.com/juspay/hyperswitch/pull/3084)) ([`a7b688a`](https://github.com/juspay/hyperswitch/commit/a7b688aac72e15f782046b9d108aca12f43a9994)) +- Add utility to convert TOML configuration file to list of environment variables ([#3096](https://github.com/juspay/hyperswitch/pull/3096)) ([`2c4599a`](https://github.com/juspay/hyperswitch/commit/2c4599a1cd7e244b6fb11948c88c55c5b8faad76)) + +### Bug Fixes + +- **router:** Make `request_incremental_authorization` optional in payment_intent ([#3086](https://github.com/juspay/hyperswitch/pull/3086)) ([`f7da59d`](https://github.com/juspay/hyperswitch/commit/f7da59d06af11707e210b58a875c013d31c3ee17)) + +### Refactors + +- **email:** Create client every time of sending email ([#3105](https://github.com/juspay/hyperswitch/pull/3105)) ([`fc2f163`](https://github.com/juspay/hyperswitch/commit/fc2f16392148cd66b3c3e67e3e0c782910e37e1f)) + +### Testing + +- **postman:** Update postman collection files ([`aa97821`](https://github.com/juspay/hyperswitch/commit/aa9782164fb7846fe533c5057a17756dc82ede54)) + +### Miscellaneous Tasks + +- **deps:** Update fred and moka ([#3088](https://github.com/juspay/hyperswitch/pull/3088)) ([`129b1e5`](https://github.com/juspay/hyperswitch/commit/129b1e55bd1cbad0243030fd25379f1400eb170c)) + +**Full Changelog:** [`v1.98.0...v1.99.0`](https://github.com/juspay/hyperswitch/compare/v1.98.0...v1.99.0) + +- - - + + +## 1.98.0 (2023-12-11) + +### Features + +- **connector:** Accept connector_transaction_id in error_response of connector flows for Trustpay ([#3060](https://github.com/juspay/hyperswitch/pull/3060)) ([`f53b090`](https://github.com/juspay/hyperswitch/commit/f53b090db87e094f9694481f13af62240c4c422a)) +- **pm_auth:** Pm_auth service migration ([#3047](https://github.com/juspay/hyperswitch/pull/3047)) ([`9c1c44a`](https://github.com/juspay/hyperswitch/commit/9c1c44a706750b14857e9180f5161b61ed89a2ad)) +- **user:** Add `verify_email` API ([#3076](https://github.com/juspay/hyperswitch/pull/3076)) ([`585e009`](https://github.com/juspay/hyperswitch/commit/585e00980c43797f326efb809df9ffd497d1dd26)) +- **users:** Add resend verification email API ([#3093](https://github.com/juspay/hyperswitch/pull/3093)) ([`6d5c25e`](https://github.com/juspay/hyperswitch/commit/6d5c25e3369117acaf5865965769649d524226af)) + +### Bug Fixes + +- **analytics:** Adding api_path to api logs event and to auditlogs api response ([#3079](https://github.com/juspay/hyperswitch/pull/3079)) ([`bf67438`](https://github.com/juspay/hyperswitch/commit/bf674380d5c7e856d0bae75554326aa9017c0201)) +- **config:** Add missing config fields in `docker_compose.toml` ([#3080](https://github.com/juspay/hyperswitch/pull/3080)) ([`1f8116d`](https://github.com/juspay/hyperswitch/commit/1f8116db368aec344d08603045c4cb46c2c25b41)) +- **connector:** [CYBERSOURCE] Remove Phone Number Field From Address ([#3095](https://github.com/juspay/hyperswitch/pull/3095)) ([`72955ec`](https://github.com/juspay/hyperswitch/commit/72955ecc68280773b9c77b4db3d46de95a62f9ed)) +- **drainer:** Properly log deserialization errors ([#3075](https://github.com/juspay/hyperswitch/pull/3075)) ([`42b5bd4`](https://github.com/juspay/hyperswitch/commit/42b5bd4f3d142c9fa12475f36a8b144753ac06e2)) +- **router:** Allow zero amount for payment intent in list payment methods ([#3090](https://github.com/juspay/hyperswitch/pull/3090)) ([`b283b6b`](https://github.com/juspay/hyperswitch/commit/b283b6b662c9f2eabe90473434369d8f7c2369a6)) +- **user:** Add checks for change password ([#3078](https://github.com/juspay/hyperswitch/pull/3078)) ([`26a2611`](https://github.com/juspay/hyperswitch/commit/26a261131b4dbb8570e139127a2c0d356e2820be)) + +### Refactors + +- **payment_methods:** Make the card_holder_name optional for card details in the payment APIs ([#3074](https://github.com/juspay/hyperswitch/pull/3074)) ([`b279591`](https://github.com/juspay/hyperswitch/commit/b279591057cdba6004c99efc82bb856f0bacd1e0)) +- **user:** Add account verification check in signin ([#3082](https://github.com/juspay/hyperswitch/pull/3082)) ([`f7d6e3c`](https://github.com/juspay/hyperswitch/commit/f7d6e3c0149869175a59996e67d3e2d3b6f3b8c2)) + +### Documentation + +- **openapi:** Fix `payment_methods_enabled` OpenAPI spec in merchant connector account APIs ([#3068](https://github.com/juspay/hyperswitch/pull/3068)) ([`b6838c4`](https://github.com/juspay/hyperswitch/commit/b6838c4d1a3a456e28a5f438fcd74a60bedb2539)) + +### Miscellaneous Tasks + +- **configs:** [CYBERSOURCE] Add mandate configs ([#3085](https://github.com/juspay/hyperswitch/pull/3085)) ([`777cd5c`](https://github.com/juspay/hyperswitch/commit/777cd5cdc2342fb7195a06505647fa331725e1dd)) + +**Full Changelog:** [`v1.97.0...v1.98.0`](https://github.com/juspay/hyperswitch/compare/v1.97.0...v1.98.0) + +- - - + + +## 1.97.0 (2023-12-06) + +### Features + +- **Braintree:** Sync with Hyperswitch Reference ([#3037](https://github.com/juspay/hyperswitch/pull/3037)) ([`8a995ce`](https://github.com/juspay/hyperswitch/commit/8a995cefdf6806645383710c6f39d963da232e94)) +- **connector:** [BANKOFAMERICA] Implement Apple Pay ([#3061](https://github.com/juspay/hyperswitch/pull/3061)) ([`47c0383`](https://github.com/juspay/hyperswitch/commit/47c038300adad1c02e4c77d529c7cc2457cf3b91)) +- **metrics:** Add drainer delay metric ([#3034](https://github.com/juspay/hyperswitch/pull/3034)) ([`c6e2ee2`](https://github.com/juspay/hyperswitch/commit/c6e2ee29d9ee4fe54e6fa6f87c2fa065a290d258)) + +### Bug Fixes + +- **config:** Parse kafka brokers from env variable as sequence ([#3066](https://github.com/juspay/hyperswitch/pull/3066)) ([`84decd8`](https://github.com/juspay/hyperswitch/commit/84decd8126d306a5e1cf22b36e1378a73dc963f5)) +- Throw bad request while pushing duplicate data to redis ([#3016](https://github.com/juspay/hyperswitch/pull/3016)) ([`a2405e5`](https://github.com/juspay/hyperswitch/commit/a2405e56fbd84936a1afa6aa9f8f7e815267fbec)) +- Return url none on complete authorize ([#3067](https://github.com/juspay/hyperswitch/pull/3067)) ([`6eec06b`](https://github.com/juspay/hyperswitch/commit/6eec06b1d6ee9a00b374905e0ab9e425d0e41095)) + +### Miscellaneous Tasks + +- **codeowners:** Add codeowners for hyperswitch dashboard ([#3057](https://github.com/juspay/hyperswitch/pull/3057)) ([`cfafd5c`](https://github.com/juspay/hyperswitch/commit/cfafd5cd29857283d57731dda7c5a332a493f531)) + +**Full Changelog:** [`v1.96.0...v1.97.0`](https://github.com/juspay/hyperswitch/compare/v1.96.0...v1.97.0) + +- - - + + +## 1.96.0 (2023-12-05) + +### Features + +- **connector_onboarding:** Add Connector onboarding APIs ([#3050](https://github.com/juspay/hyperswitch/pull/3050)) ([`7bd6e05`](https://github.com/juspay/hyperswitch/commit/7bd6e05c0c05ebae9b82a6f410e61ca4409d088b)) +- **pm_list:** Add required fields for bancontact_card for Mollie, Adyen and Stripe ([#3035](https://github.com/juspay/hyperswitch/pull/3035)) ([`792e642`](https://github.com/juspay/hyperswitch/commit/792e642ad58f90bae3ddcea5e6cbc70e948d8e28)) +- **user:** Add email apis and new enums for metadata ([#3053](https://github.com/juspay/hyperswitch/pull/3053)) ([`1c3d260`](https://github.com/juspay/hyperswitch/commit/1c3d260dc3e18fbf6cbd5122122a6c73dceb39a3)) +- Implement FRM flows ([#2968](https://github.com/juspay/hyperswitch/pull/2968)) ([`055d838`](https://github.com/juspay/hyperswitch/commit/055d8383671f6b466297c177bcc770618c7da96a)) + +### Bug Fixes + +- Remove redundant call to populate_payment_data function ([#3054](https://github.com/juspay/hyperswitch/pull/3054)) ([`53df543`](https://github.com/juspay/hyperswitch/commit/53df543b7f1407a758232025b7de0fb527be8e86)) + +### Documentation + +- **test_utils:** Update postman docs ([#3055](https://github.com/juspay/hyperswitch/pull/3055)) ([`8b7a7aa`](https://github.com/juspay/hyperswitch/commit/8b7a7aa6494ff669e1f8bcc92a5160e422d6b26e)) + +**Full Changelog:** [`v1.95.0...v1.96.0`](https://github.com/juspay/hyperswitch/compare/v1.95.0...v1.96.0) + +- - - + + +## 1.95.0 (2023-12-05) + +### Features + +- **connector:** [BOA/CYBERSOURCE] Fix Status Mapping for Terminal St… ([#3031](https://github.com/juspay/hyperswitch/pull/3031)) ([`95876b0`](https://github.com/juspay/hyperswitch/commit/95876b0ce03e024edf77909502c53eb4e63a9855)) +- **pm_list:** Add required field for open_banking_uk for Adyen and Volt Connector ([#3032](https://github.com/juspay/hyperswitch/pull/3032)) ([`9d93533`](https://github.com/juspay/hyperswitch/commit/9d935332193dcc9f191a0a5a9e7405316794a418)) +- **router:** + - Add key_value to locker metrics ([#2995](https://github.com/juspay/hyperswitch/pull/2995)) ([`83fcd1a`](https://github.com/juspay/hyperswitch/commit/83fcd1a9deb106a44c8262923c7f1660b0c46bf2)) + - Add payments incremental authorization api ([#3038](https://github.com/juspay/hyperswitch/pull/3038)) ([`a0cfdd3`](https://github.com/juspay/hyperswitch/commit/a0cfdd3fb12f04b603f65551eac985c31e08da85)) +- **types:** Add email types for sending emails ([#3020](https://github.com/juspay/hyperswitch/pull/3020)) ([`c4bd47e`](https://github.com/juspay/hyperswitch/commit/c4bd47eca93a158c9daeeeb18afb1e735eea8c94)) +- **user:** + - Generate and delete sample data ([#2987](https://github.com/juspay/hyperswitch/pull/2987)) ([`092ec73`](https://github.com/juspay/hyperswitch/commit/092ec73b3c65ce6048d379383b078d643f0f35fc)) + - Add user_list and switch_list apis ([#3033](https://github.com/juspay/hyperswitch/pull/3033)) ([`ec15ddd`](https://github.com/juspay/hyperswitch/commit/ec15ddd0d0ed942fedec525406df3005d494b8d4)) +- Calculate surcharge for customer saved card list ([#3039](https://github.com/juspay/hyperswitch/pull/3039)) ([`daf0f09`](https://github.com/juspay/hyperswitch/commit/daf0f09f8e3293ee6a3599a25362d9171fc5b2e7)) + +### Bug Fixes + +- **connector:** [Paypal] Parse response for Cards with no 3DS check ([#3021](https://github.com/juspay/hyperswitch/pull/3021)) ([`d883cd1`](https://github.com/juspay/hyperswitch/commit/d883cd18972c5f9e8350e9a3f4e5cd56ec2c0787)) +- **pm_list:** [Trustpay]Update dynamic fields for trustpay blik ([#3042](https://github.com/juspay/hyperswitch/pull/3042)) ([`9274cef`](https://github.com/juspay/hyperswitch/commit/9274cefbdd29d2ac64baeea2fe504dff2472cb47)) +- **wasm:** Fix wasm function to return the categories for keys with their description respectively ([#3023](https://github.com/juspay/hyperswitch/pull/3023)) ([`2ac5b2c`](https://github.com/juspay/hyperswitch/commit/2ac5b2cd764c0aad53ac7c672dfcc9132fa5668f)) +- Use card bin to get additional card details ([#3036](https://github.com/juspay/hyperswitch/pull/3036)) ([`6c7d3a2`](https://github.com/juspay/hyperswitch/commit/6c7d3a2e8a047ff23b52b76792fe8f28d3b952a4)) +- Transform connector name to lowercase in connector integration script ([#3048](https://github.com/juspay/hyperswitch/pull/3048)) ([`298e362`](https://github.com/juspay/hyperswitch/commit/298e3627c379de5acfcafb074036754661801f1e)) +- Add fallback to reverselookup error ([#3025](https://github.com/juspay/hyperswitch/pull/3025)) ([`ba392f5`](https://github.com/juspay/hyperswitch/commit/ba392f58b2956d67e93a08853bcf2270a869be27)) + +### Refactors + +- **payment_methods:** Add support for passing card_cvc in payment_method_data object along with token ([#3024](https://github.com/juspay/hyperswitch/pull/3024)) ([`3ce04ab`](https://github.com/juspay/hyperswitch/commit/3ce04abae4eddfa27025368f5ef28987cccea43d)) +- **users:** Separate signup and signin ([#2921](https://github.com/juspay/hyperswitch/pull/2921)) ([`80efeb7`](https://github.com/juspay/hyperswitch/commit/80efeb76b1801529766978af1c06e2d2c7de66c0)) +- Create separate struct for surcharge details response ([#3027](https://github.com/juspay/hyperswitch/pull/3027)) ([`57591f8`](https://github.com/juspay/hyperswitch/commit/57591f819c7994099e76cff1affc7bcf3e45a031)) + +### Testing + +- **postman:** Update postman collection files ([`6e09bc9`](https://github.com/juspay/hyperswitch/commit/6e09bc9e2c4bbe14dcb70da4a438850b03b3254c)) + +**Full Changelog:** [`v1.94.0...v1.95.0`](https://github.com/juspay/hyperswitch/compare/v1.94.0...v1.95.0) + +- - - + + +## 1.94.0 (2023-12-01) + +### Features + +- **user_role:** Add APIs for user roles ([#3013](https://github.com/juspay/hyperswitch/pull/3013)) ([`3fa0bdf`](https://github.com/juspay/hyperswitch/commit/3fa0bdf76558ec91df8d3beef3c36658cd138b37)) + +### Bug Fixes + +- **config:** Add kms decryption support for sqlx password ([#3029](https://github.com/juspay/hyperswitch/pull/3029)) ([`b593467`](https://github.com/juspay/hyperswitch/commit/b5934674e518f991a8a575ad01b971dd086eeb40)) + +### Refactors + +- **connector:** + - [Multisafe Pay] change error message from not supported to not implemented ([#2851](https://github.com/juspay/hyperswitch/pull/2851)) ([`668b943`](https://github.com/juspay/hyperswitch/commit/668b943403df2b3bb354dd093b8ec073a2618bda)) + - [Shift4] change error message from NotSupported to NotImplemented ([#2880](https://github.com/juspay/hyperswitch/pull/2880)) ([`bc79d52`](https://github.com/juspay/hyperswitch/commit/bc79d522c30aa036378cf1e01354c422585cc226)) + +**Full Changelog:** [`v1.93.0...v1.94.0`](https://github.com/juspay/hyperswitch/compare/v1.93.0...v1.94.0) + +- - - + + +## 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 diff --git a/Cargo.lock b/Cargo.lock index a03340093c88..f0334ce9cfc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ "futures-sink", "memchr", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", "tracing", ] @@ -44,8 +44,8 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "ahash 0.8.3", - "base64 0.21.4", + "ahash 0.8.6", + "base64 0.21.5", "bitflags 1.3.2", "brotli", "bytes 1.5.0", @@ -67,7 +67,7 @@ dependencies = [ "rand 0.8.5", "sha1", "smallvec 1.11.1", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", "tracing", "zstd", @@ -80,7 +80,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -105,7 +105,7 @@ dependencies = [ "serde_json", "serde_plain", "tempfile", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -118,7 +118,7 @@ dependencies = [ "parse-size", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -142,7 +142,7 @@ checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" dependencies = [ "actix-macros", "futures-core", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -156,9 +156,9 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio 0.8.8", - "socket2 0.5.4", - "tokio 1.32.0", + "mio 0.8.10", + "socket2 0.5.5", + "tokio 1.35.1", "tracing", ] @@ -188,11 +188,11 @@ dependencies = [ "pin-project-lite", "rustls 0.21.7", "rustls-webpki", - "tokio 1.32.0", - "tokio-rustls", + "tokio 1.35.1", + "tokio-rustls 0.23.4", "tokio-util", "tracing", - "webpki-roots", + "webpki-roots 0.22.6", ] [[package]] @@ -220,7 +220,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash 0.7.6", + "ahash 0.7.7", "bytes 1.5.0", "bytestring", "cfg-if 1.0.0", @@ -255,7 +255,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -281,25 +281,26 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if 1.0.0", - "getrandom 0.2.10", + "getrandom 0.2.11", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -332,6 +333,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.35.1", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -375,13 +406,15 @@ dependencies = [ "common_utils", "error-stack", "euclid", + "frunk", + "frunk_core", "masking", "mime", "reqwest", "router_derive", "serde", "serde_json", - "strum 0.24.1", + "strum 0.25.0", "time", "url", "utoipa", @@ -393,12 +426,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" -[[package]] -name = "arcstr" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f907281554a3d0312bb7aab855a8e0ef6cbf1614d06de54105039ca8b34460e" - [[package]] name = "argon2" version = "0.5.2" @@ -481,7 +508,7 @@ dependencies = [ "bb8", "diesel", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", "tracing", ] @@ -506,27 +533,7 @@ dependencies = [ "futures-core", "memchr", "pin-project-lite", - "tokio 1.32.0", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock", - "autocfg", - "cfg-if 1.0.0", - "concurrent-queue", - "futures-lite", - "log", - "parking", - "polling", - "rustix 0.37.25", - "slab", - "socket2 0.4.9", - "waker-fn", + "tokio 1.35.1", ] [[package]] @@ -557,18 +564,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -604,8 +611,8 @@ dependencies = [ "actix-service", "actix-tls", "actix-utils", - "ahash 0.7.6", - "base64 0.21.4", + "ahash 0.7.7", + "base64 0.21.5", "bytes 1.5.0", "cfg-if 1.0.0", "cookie", @@ -624,7 +631,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -651,7 +658,7 @@ dependencies = [ "hyper", "ring", "time", - "tokio 1.32.0", + "tokio 1.35.1", "tower", "tracing", "zeroize", @@ -666,7 +673,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-types", "fastrand 1.9.0", - "tokio 1.32.0", + "tokio 1.35.1", "tracing", "zeroize", ] @@ -729,6 +736,31 @@ dependencies = [ "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", + "tower", + "tracing", +] + [[package]] name = "aws-sdk-s3" version = "0.28.0" @@ -882,7 +914,7 @@ checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880" dependencies = [ "futures-util", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-stream", ] @@ -922,11 +954,11 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls", + "hyper-rustls 0.23.2", "lazy_static", "pin-project-lite", "rustls 0.20.9", - "tokio 1.32.0", + "tokio 1.35.1", "tower", "tracing", ] @@ -960,7 +992,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "pin-utils", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", "tracing", ] @@ -1106,9 +1138,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64-simd" @@ -1136,7 +1168,7 @@ dependencies = [ "futures-channel", "futures-util", "parking_lot 0.12.1", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -1148,6 +1180,7 @@ dependencies = [ "num-bigint", "num-integer", "num-traits", + "serde", ] [[package]] @@ -1186,6 +1219,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" @@ -1227,6 +1272,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.48", + "syn_derive", +] + [[package]] name = "brotli" version = "3.4.0" @@ -1264,6 +1333,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" @@ -1334,6 +1425,7 @@ dependencies = [ "error-stack", "luhn", "masking", + "router_env", "serde", "serde_json", "thiserror", @@ -1415,6 +1507,12 @@ 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" @@ -1433,7 +1531,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1487,9 +1585,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.4" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed" +checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2" dependencies = [ "clap_builder", "clap_derive", @@ -1498,9 +1596,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.4" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636" +checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" dependencies = [ "anstyle", "bitflags 1.3.2", @@ -1516,7 +1614,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -1571,7 +1669,7 @@ dependencies = [ "once_cell", "phonenumber", "proptest", - "quick-xml", + "quick-xml 0.28.2", "rand 0.8.5", "regex", "reqwest", @@ -1587,7 +1685,7 @@ dependencies = [ "test-case", "thiserror", "time", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -1618,6 +1716,29 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "config_importer" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "indexmap 2.1.0", + "serde", + "serde_json", + "toml 0.7.4", +] + +[[package]] +name = "connector_configs" +version = "0.1.0" +dependencies = [ + "api_models", + "serde", + "serde_with", + "toml 0.7.4", + "utoipa", +] + [[package]] name = "constant_time_eq" version = "0.2.6" @@ -1858,6 +1979,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "currency_conversion" +version = "0.1.0" +dependencies = [ + "common_enums", + "rust_decimal", + "rusty-money", + "serde", + "thiserror", +] + [[package]] name = "darling" version = "0.14.4" @@ -1903,7 +2035,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -1925,7 +2057,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -1935,7 +2067,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if 1.0.0", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "lock_api 0.4.10", "once_cell", "parking_lot_core 0.9.8", @@ -1955,6 +2087,7 @@ dependencies = [ "async-trait", "common_enums", "common_utils", + "diesel_models", "error-stack", "masking", "serde", @@ -1973,7 +2106,7 @@ dependencies = [ "deadpool-runtime", "num_cpus", "retain_mut", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -2006,6 +2139,37 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "derive_deref" version = "1.1.1" @@ -2061,7 +2225,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -2091,7 +2255,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -2148,7 +2312,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -2168,6 +2332,7 @@ name = "drainer" version = "0.1.0" dependencies = [ "async-bb8-diesel", + "async-trait", "bb8", "clap", "common_utils", @@ -2184,7 +2349,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -2225,23 +2390,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -2281,6 +2435,7 @@ dependencies = [ "serde_json", "strum 0.25.0", "thiserror", + "utoipa", ] [[package]] @@ -2299,8 +2454,11 @@ name = "euclid_wasm" version = "0.1.0" dependencies = [ "api_models", + "common_enums", + "connector_configs", + "currency_conversion", "euclid", - "getrandom 0.2.10", + "getrandom 0.2.11", "kgraph_utils", "once_cell", "ron-parser", @@ -2323,18 +2481,24 @@ dependencies = [ "async-trait", "aws-config", "aws-sdk-kms", + "aws-sdk-s3", "aws-sdk-sesv2", + "aws-sdk-sts", "aws-smithy-client", - "base64 0.21.4", + "base64 0.21.5", "common_utils", "dyn-clone", "error-stack", + "hex", + "hyper", + "hyper-proxy", "masking", "once_cell", "router_env", "serde", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", + "vaultrs", ] [[package]] @@ -2359,12 +2523,12 @@ dependencies = [ "futures-util", "http", "hyper", - "hyper-rustls", + "hyper-rustls 0.23.2", "mime", "serde", "serde_json", "time", - "tokio 1.32.0", + "tokio 1.35.1", "url", "webdriver", ] @@ -2441,16 +2605,14 @@ dependencies = [ [[package]] name = "fred" -version = "6.3.2" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15cc18b56395b8b15ffcdcea7fe8586e3a3ccb3d9dc3b9408800d9814efb08e" +checksum = "9282e65613822eea90c99872c51afa1de61542215cb11f91456a93f50a5a131a" dependencies = [ "arc-swap", - "arcstr", "async-trait", "bytes 1.5.0", "bytes-utils", - "cfg-if 1.0.0", "float-cmp", "futures 0.3.28", "lazy_static", @@ -2459,13 +2621,14 @@ dependencies = [ "rand 0.8.5", "redis-protocol", "semver 1.0.19", - "sha-1 0.10.1", - "tokio 1.32.0", + "socket2 0.5.5", + "tokio 1.35.1", "tokio-stream", "tokio-util", "tracing", "tracing-futures", "url", + "urlencoding", ] [[package]] @@ -2493,7 +2656,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e" dependencies = [ "frunk_proc_macro_helpers", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -2505,7 +2668,7 @@ dependencies = [ "frunk_core", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -2517,7 +2680,7 @@ dependencies = [ "frunk_core", "frunk_proc_macro_helpers", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -2536,6 +2699,12 @@ 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" @@ -2559,9 +2728,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -2569,9 +2738,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" @@ -2597,9 +2766,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -2618,26 +2787,26 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-timer" @@ -2647,9 +2816,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -2680,7 +2849,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -2696,9 +2865,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -2768,9 +2937,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes 1.5.0", "fnv", @@ -2778,9 +2947,9 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", "tracing", ] @@ -2797,16 +2966,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.6", + "ahash 0.7.7", ] [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.6", "allocator-api2", ] @@ -2816,7 +2985,31 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.1", + "hashbrown 0.14.3", +] + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.5", + "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]] @@ -2940,12 +3133,30 @@ dependencies = [ "itoa", "pin-project-lite", "socket2 0.4.9", - "tokio 1.32.0", + "tokio 1.35.1", "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.35.1", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.23.2" @@ -2957,8 +3168,22 @@ dependencies = [ "log", "rustls 0.20.9", "rustls-native-certs", - "tokio 1.32.0", - "tokio-rustls", + "tokio 1.35.1", + "tokio-rustls 0.23.4", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls 0.21.7", + "tokio 1.35.1", + "tokio-rustls 0.24.1", ] [[package]] @@ -2969,7 +3194,7 @@ checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-io-timeout", ] @@ -2982,22 +3207,22 @@ dependencies = [ "bytes 1.5.0", "hyper", "native-tls", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-native-tls", ] [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -3080,12 +3305,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "serde", ] @@ -3113,17 +3338,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys", -] - [[package]] name = "iovec" version = "0.1.4" @@ -3146,8 +3360,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.17", - "windows-sys", + "rustix", + "windows-sys 0.48.0", ] [[package]] @@ -3190,7 +3404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33a96c4f2128a6f44ecf7c36df2b03dddf5a07b060a4d5ebc0a81e9821f7c60e" dependencies = [ "anyhow", - "base64 0.21.4", + "base64 0.21.5", "flate2", "once_cell", "openssl", @@ -3236,7 +3450,7 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "pem", "ring", "serde", @@ -3259,6 +3473,7 @@ name = "kgraph_utils" version = "0.1.0" dependencies = [ "api_models", + "common_enums", "criterion", "euclid", "masking", @@ -3281,9 +3496,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libgit2-sys" @@ -3333,15 +3548,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "local-channel" @@ -3418,6 +3627,7 @@ version = "0.1.0" dependencies = [ "bytes 1.5.0", "diesel", + "erased-serde", "serde", "serde_json", "subtle", @@ -3589,14 +3799,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3624,12 +3834,12 @@ dependencies = [ [[package]] name = "moka" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6e72583bf6830c956235bff0d5afec8cf2952f579ebad18ae7821a917d950f" +checksum = "d8017ec3548ffe7d4cef7ac0e12b044c01164a74c0f3119420faeaf13490ad8b" dependencies = [ - "async-io", "async-lock", + "async-trait", "crossbeam-channel", "crossbeam-epoch 0.9.15", "crossbeam-utils 0.8.16", @@ -3638,7 +3848,6 @@ dependencies = [ "parking_lot 0.12.1", "quanta", "rustc_version 0.4.0", - "scheduled-thread-pool", "skeptic", "smallvec 1.11.1", "tagptr", @@ -3647,6 +3856,12 @@ dependencies = [ "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" @@ -3750,9 +3965,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -3768,6 +3983,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" @@ -3810,11 +4046,20 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openapi" +version = "0.1.0" +dependencies = [ + "api_models", + "serde_json", + "utoipa", +] + [[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 1.0.0", @@ -3833,7 +4078,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -3844,9 +4089,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", @@ -3878,7 +4123,7 @@ dependencies = [ "opentelemetry-proto", "prost", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", "tonic", ] @@ -3929,7 +4174,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-stream", ] @@ -4032,7 +4277,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec 1.11.1", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -4119,7 +4364,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -4183,7 +4428,7 @@ dependencies = [ "itertools 0.11.0", "lazy_static", "nom", - "quick-xml", + "quick-xml 0.28.2", "regex", "regex-cache", "serde", @@ -4209,7 +4454,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -4258,6 +4503,27 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "pm_auth" +version = "0.1.0" +dependencies = [ + "api_models", + "async-trait", + "bytes 1.5.0", + "common_enums", + "common_utils", + "error-stack", + "http", + "masking", + "mime", + "router_derive", + "router_env", + "serde", + "serde_json", + "strum 0.24.1", + "thiserror", +] + [[package]] name = "png" version = "0.16.8" @@ -4270,22 +4536,6 @@ dependencies = [ "miniz_oxide 0.3.7", ] -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if 1.0.0", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys", -] - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -4301,6 +4551,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.14", +] + +[[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" @@ -4327,9 +4596,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.68" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -4377,6 +4646,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" @@ -4430,11 +4719,21 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -4450,6 +4749,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" @@ -4509,7 +4814,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", ] [[package]] @@ -4559,6 +4864,36 @@ dependencies = [ "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.35.1", +] + +[[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]] name = "redis-protocol" version = "4.1.0" @@ -4584,7 +4919,8 @@ dependencies = [ "router_env", "serde", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", + "tokio-stream", ] [[package]] @@ -4617,7 +4953,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", "redox_syscall 0.2.16", "thiserror", ] @@ -4678,6 +5014,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" @@ -4685,7 +5030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "async-compression", - "base64 0.21.4", + "base64 0.21.5", "bytes 1.5.0", "encoding_rs", "futures-core", @@ -4694,6 +5039,7 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-rustls 0.24.2", "hyper-tls", "ipnet", "js-sys", @@ -4704,18 +5050,22 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.21.7", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "system-configuration", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-native-tls", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots 0.25.3", "winreg", ] @@ -4740,6 +5090,34 @@ dependencies = [ "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]] name = "ron" version = "0.7.1" @@ -4773,14 +5151,13 @@ dependencies = [ "actix-multipart", "actix-rt", "actix-web", + "analytics", "api_models", "argon2", "async-bb8-diesel", "async-trait", "awc", - "aws-config", - "aws-sdk-s3", - "base64 0.21.4", + "base64 0.21.5", "bb8", "bigdecimal", "blake3", @@ -4790,6 +5167,7 @@ dependencies = [ "common_enums", "common_utils", "config", + "currency_conversion", "data_models", "derive_deref", "diesel", @@ -4797,6 +5175,7 @@ dependencies = [ "digest 0.9.0", "dyn-clone", "encoding_rs", + "erased-serde", "error-stack", "euclid", "external_services", @@ -4816,10 +5195,14 @@ dependencies = [ "nanoid", "num_cpus", "once_cell", + "openapi", "openssl", + "pm_auth", "qrcode", + "quick-xml 0.31.0", "rand 0.8.5", "rand_chacha 0.3.1", + "rdkafka", "redis_interface", "regex", "reqwest", @@ -4827,6 +5210,7 @@ dependencies = [ "router_derive", "router_env", "roxmltree", + "rust_decimal", "rustc-hash", "scheduler", "serde", @@ -4836,20 +5220,19 @@ dependencies = [ "serde_urlencoded", "serde_with", "serial_test", - "sha-1 0.9.8", + "sha-1", "sqlx", "storage_impl", - "strum 0.24.1", + "strum 0.25.0", "tera", "test_utils", "thiserror", "time", - "tokio 1.32.0", + "tokio 1.35.1", "tracing-futures", "unicode-segmentation", "url", "utoipa", - "utoipa-swagger-ui", "uuid", "validator", "wiremock", @@ -4860,15 +5243,14 @@ dependencies = [ name = "router_derive" version = "0.1.0" dependencies = [ - "darling 0.14.4", "diesel", - "indexmap 2.0.2", + "indexmap 2.1.0", "proc-macro2", "quote", "serde", "serde_json", "strum 0.24.1", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] @@ -4888,7 +5270,7 @@ dependencies = [ "serde_path_to_error", "strum 0.24.1", "time", - "tokio 1.32.0", + "tokio 1.35.1", "tracing", "tracing-actix-web", "tracing-appender", @@ -4907,41 +5289,6 @@ dependencies = [ "xmlparser", ] -[[package]] -name = "rust-embed" -version = "6.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "6.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "shellexpand", - "syn 2.0.38", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "7.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" -dependencies = [ - "sha2", - "walkdir", -] - [[package]] name = "rust-ini" version = "0.18.0" @@ -4952,6 +5299,22 @@ dependencies = [ "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 = "rustc-demangle" version = "0.1.23" @@ -4992,30 +5355,50 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "0.37.25" +name = "rustify" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" +checksum = "e9c02e25271068de581e03ac3bb44db60165ff1a10d92b9530192ccb898bc706" dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys", + "anyhow", + "async-trait", + "bytes 1.5.0", + "http", + "reqwest", + "rustify_derive", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "rustify_derive" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58135536c18c04f4634bedad182a3f41baf33ef811cc38a3ec7b7061c57134c8" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "serde_urlencoded", + "syn 1.0.109", + "synstructure", ] [[package]] name = "rustix" -version = "0.38.17" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.8", - "windows-sys", + "linux-raw-sys", + "windows-sys 0.52.0", ] [[package]] @@ -5060,7 +5443,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", ] [[package]] @@ -5091,6 +5474,14 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rusty-money" +version = "0.4.1" +source = "git+https://github.com/varunsrin/rusty_money?rev=bbc0150742a0fff905225ff11ee09388e9babdcc#bbc0150742a0fff905225ff11ee09388e9babdcc" +dependencies = [ + "rust_decimal", +] + [[package]] name = "ryu" version = "1.0.15" @@ -5112,7 +5503,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -5135,6 +5526,7 @@ dependencies = [ "external_services", "futures 0.3.28", "masking", + "num_cpus", "once_cell", "rand 0.8.5", "redis_interface", @@ -5145,7 +5537,7 @@ dependencies = [ "strum 0.24.1", "thiserror", "time", - "tokio 1.32.0", + "tokio 1.35.1", "uuid", ] @@ -5171,6 +5563,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" @@ -5220,9 +5618,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] @@ -5240,22 +5638,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -5310,7 +5708,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -5336,15 +5734,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_json", "serde_with_macros", @@ -5353,14 +5751,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" dependencies = [ "darling 0.20.3", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -5385,7 +5783,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -5401,17 +5799,6 @@ dependencies = [ "opaque-debug", ] -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.10.7", -] - [[package]] name = "sha1" version = "0.10.6" @@ -5443,15 +5830,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shellexpand" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" -dependencies = [ - "dirs", -] - [[package]] name = "signal-hook" version = "0.3.17" @@ -5480,9 +5858,15 @@ dependencies = [ "futures-core", "libc", "signal-hook", - "tokio 1.32.0", + "tokio 1.35.1", ] +[[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" @@ -5561,12 +5945,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -5602,7 +5986,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" dependencies = [ - "ahash 0.7.6", + "ahash 0.7.7", "atoi", "base64 0.13.1", "bigdecimal", @@ -5676,7 +6060,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" dependencies = [ "native-tls", "once_cell", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-native-tls", ] @@ -5711,7 +6095,7 @@ dependencies = [ "serde", "serde_json", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -5781,7 +6165,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -5803,15 +6187,27 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", "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.48", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -5857,6 +6253,12 @@ 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" @@ -5866,8 +6268,8 @@ dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.1", "redox_syscall 0.3.5", - "rustix 0.38.17", - "windows-sys", + "rustix", + "windows-sys 0.48.0", ] [[package]] @@ -5911,7 +6313,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -5923,7 +6325,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", "test-case-core", ] @@ -5932,7 +6334,7 @@ name = "test_utils" version = "0.1.0" dependencies = [ "async-trait", - "base64 0.21.4", + "base64 0.21.5", "clap", "masking", "rand 0.8.5", @@ -5943,7 +6345,7 @@ dependencies = [ "serial_test", "thirtyfour", "time", - "tokio 1.32.0", + "tokio 1.35.1", "toml 0.7.4", ] @@ -5968,7 +6370,7 @@ dependencies = [ "stringmatch", "thirtyfour-macros", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", "url", "urlparse", ] @@ -6002,7 +6404,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -6028,9 +6430,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" dependencies = [ "itoa", "serde", @@ -6046,9 +6448,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" dependencies = [ "time-core", ] @@ -6104,21 +6506,21 @@ dependencies = [ [[package]] name = "tokio" -version = "1.32.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes 1.5.0", "libc", - "mio 0.8.8", + "mio 0.8.10", "num_cpus", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -6181,18 +6583,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -6202,7 +6604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -6231,10 +6633,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ "rustls 0.20.9", - "tokio 1.32.0", + "tokio 1.35.1", "webpki", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.7", + "tokio 1.35.1", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -6243,7 +6655,8 @@ checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", + "tokio-util", ] [[package]] @@ -6342,7 +6755,7 @@ dependencies = [ "futures-core", "futures-sink", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", "tracing", ] @@ -6361,10 +6774,11 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" dependencies = [ + "indexmap 1.9.3", "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.14", ] [[package]] @@ -6378,17 +6792,28 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.10" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.8.3" @@ -6411,7 +6836,7 @@ dependencies = [ "pin-project", "prost", "prost-derive", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-stream", "tokio-util", "tower", @@ -6434,7 +6859,7 @@ dependencies = [ "pin-project-lite", "rand 0.8.5", "slab", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", "tower-layer", "tower-service", @@ -6455,11 +6880,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.36" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if 1.0.0", "log", "pin-project-lite", "tracing-attributes", @@ -6468,11 +6892,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", @@ -6493,20 +6918,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -6749,7 +7174,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d82b1bc5417102a73e8464c686eef947bdfb99fcdfc0a4f228e81afa9526470a" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_json", "utoipa-gen", @@ -6764,23 +7189,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", -] - -[[package]] -name = "utoipa-swagger-ui" -version = "3.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84614caa239fb25b2bb373a52859ffd94605ceb256eeb1d63436325cf81e3653" -dependencies = [ - "actix-web", - "mime_guess", - "regex", - "rust-embed", - "serde", - "serde_json", - "utoipa", - "zip", + "syn 2.0.48", ] [[package]] @@ -6790,7 +7199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ "atomic", - "getrandom 0.2.10", + "getrandom 0.2.11", "serde", ] @@ -6815,6 +7224,26 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vaultrs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28084ac780b443e7f3514df984a2933bd3ab39e71914d951cdf8e4d298a7c9bc" +dependencies = [ + "async-trait", + "bytes 1.5.0", + "derive_builder", + "http", + "reqwest", + "rustify", + "rustify_derive", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -6913,7 +7342,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -6947,7 +7376,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7006,6 +7435,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + [[package]] name = "weezl" version = "0.1.7" @@ -7066,12 +7501,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -7080,7 +7515,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -7089,13 +7533,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -7104,47 +7563,89 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" -version = "0.4.11" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656953b22bcbfb1ec8179d60734981d1904494ecc91f8a3f0ee5c7389bb8eb4b" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" dependencies = [ "memchr", ] @@ -7156,18 +7657,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if 1.0.0", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "wiremock" -version = "0.5.19" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f71803d3a1c80377a06221e0530be02035d5b3e854af56c6ece7ac20ac441d" +checksum = "bd7b0b5b253ebc0240d6aac6dd671c495c467420577bf634d3064ae7e6fa2b4c" dependencies = [ "assert-json-diff", "async-trait", - "base64 0.21.4", + "base64 0.21.5", "deadpool", "futures 0.3.28", "futures-timer", @@ -7178,7 +7679,7 @@ dependencies = [ "regex", "serde", "serde_json", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -7191,6 +7692,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-parser" version = "0.15.1" @@ -7224,23 +7734,31 @@ dependencies = [ ] [[package]] -name = "zeroize" -version = "1.6.0" +name = "zerocopy" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +dependencies = [ + "zerocopy-derive", +] [[package]] -name = "zip" -version = "0.6.6" +name = "zerocopy-derive" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ - "byteorder", - "crc32fast", - "crossbeam-utils 0.8.16", - "flate2", + "proc-macro2", + "quote", + "syn 2.0.48", ] +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + [[package]] name = "zstd" version = "0.12.4" 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/Makefile b/Makefile index abe0dd50b145..a39fc4c22673 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,10 @@ eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\ $(findstring $(2),$(1))),1) + +ROOT_DIR_WITH_SLASH := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) +ROOT_DIR := $(realpath $(ROOT_DIR_WITH_SLASH)) + # # = Targets # @@ -30,11 +34,18 @@ eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\ release +# Check a local package and all of its dependencies for errors +# +# Usage : +# make check +check: + cargo check + + # Compile application for running on local machine # # Usage : # make build - build : cargo build @@ -67,6 +78,14 @@ fmt : clippy : cargo clippy --all-features --all-targets -- -D warnings +# Build the DSL crate as a WebAssembly JS library +# +# Usage : +# make euclid-wasm + +euclid-wasm: + wasm-pack build --target web --out-dir $(ROOT_DIR)/wasm --out-name euclid $(ROOT_DIR)/crates/euclid_wasm -- --features dummy_connector + # Run Rust tests of project. # # Usage : @@ -93,4 +112,4 @@ precommit : fmt clippy test hack: - cargo hack check --workspace --each-feature --no-dev-deps \ No newline at end of file + cargo hack check --workspace --each-feature --all-targets diff --git a/README.md b/README.md index 129a0512d4a0..0f5e924589f2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Hyperswitch-Logo

-

The open-source payments switch

@@ -11,14 +10,16 @@ The single API to access payment ecosystems across 130+ countries

Quick Start Guide • + Local Setup GuideFast Integration for Stripe Users • + API Docs Supported Features • - FAQs
What's IncludedJoin us in building HyperSwitchCommunityBugs and feature requests • + FAQsVersioningCopyright and License

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

-
@@ -57,17 +57,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 +72,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 +111,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 +265,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 +277,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 +325,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/add_connector.md b/add_connector.md index da09ae0024e7..7fc3dcb27d14 100644 --- a/add_connector.md +++ b/add_connector.md @@ -9,6 +9,14 @@ This is a guide to contributing new connector to Router. This guide includes ins - Understanding of the Connector APIs which you wish to integrate with Router - Setup of Router repository and running it on local - Access to API credentials for testing the Connector API (you can quickly sign up for sandbox/uat credentials by visiting the website of the connector you wish to integrate) +- Ensure that you have the nightly toolchain installed because the connector template script includes code formatting. + + Install it using `rustup`: + + ```bash + rustup toolchain install nightly + ``` + In Router, there are Connectors and Payment Methods, examples of both are shown below from which the difference is apparent. @@ -17,22 +25,17 @@ In Router, there are Connectors and Payment Methods, examples of both are shown A connector is an integration to fulfill payments. Related use cases could be any of the below - Payment processor (Stripe, Adyen, ChasePaymentTech etc.,) -- Fraud and Risk management platform (like Ravelin, Riskified etc.,) +- Fraud and Risk management platform (like Signifyd, Riskified etc.,) - Payment network (Visa, Master) - Payment authentication services (Cardinal etc.,) - Router supports "Payment Processors" right now. Support will be extended to the other categories in the near future. +Currently, the router is compatible with 'Payment Processors' and 'Fraud and Risk Management' platforms. Support for additional categories will be expanded in the near future. ### What is a Payment Method ? -Each Connector (say, a Payment Processor) could support multiple payment methods +Every Payment Processor has the capability to accommodate various payment methods. Refer to the [Hyperswitch Payment matrix](https://hyperswitch.io/pm-list) to discover the supported processors and payment methods. -- **Cards :** Bancontact , Knet, Mada -- **Bank Transfers :** EPS , giropay, sofort -- **Bank Direct Debit :** Sepa direct debit -- **Wallets :** Apple Pay , Google Pay , Paypal - -Cards and Bank Transfer payment methods are already included in Router. Hence, adding a new connector which offers payment_methods available in Router is easy and requires almost no breaking changes. -Adding a new payment method (say Wallets or Bank Direct Debit) might require some changes in core business logic of Router, which we are actively working upon. +The above mentioned payment methods are already included in Router. Hence, adding a new connector which offers payment_methods available in Router is easy and requires almost no breaking changes. +Adding a new payment method might require some changes in core business logic of Router, which we are actively working upon. ## How to Integrate a Connector @@ -46,8 +49,7 @@ Below is a step-by-step tutorial for integrating a new connector. ### **Generate the template** ```bash -cd scripts -bash add_connector.sh +sh scripts/add_connector.sh ``` For this tutorial `` would be `checkout`. @@ -81,50 +83,59 @@ For example, in case of checkout, the [request](https://api-reference.checkout.c Now let's implement Request type for checkout -```rust -#[derive(Debug, Serialize)] -pub struct CheckoutPaymentsRequest { - pub source: Source, - pub amount: i64, - pub currency: String, - #[serde(default = "generate_processing_channel_id")] - pub processing_channel_id: Cow<'static, str>, -} - -fn generate_processing_channel_id() -> Cow<'static, str> { - "pc_e4mrdrifohhutfurvuawughfwu".into() -} -``` - -Since Router is connector agnostic, only minimal data is sent to connector and optional fields may be ignored. - -Here processing_channel_id, is specific to checkout and implementations of such functions should be inside the checkout directory. -Let's define `Source` - ```rust #[derive(Debug, Serialize)] pub struct CardSource { #[serde(rename = "type")] - pub source_type: Option, - pub number: Option, - pub expiry_month: Option, - pub expiry_year: Option, + pub source_type: CheckoutSourceTypes, + pub number: cards::CardNumber, + pub expiry_month: Secret, + pub expiry_year: Secret, + pub cvv: Secret, } #[derive(Debug, Serialize)] #[serde(untagged)] -pub enum Source { +pub enum PaymentSource { Card(CardSource), - // TODO: Add other sources here. + Wallets(WalletSource), + ApplePayPredecrypt(Box), +} + +#[derive(Debug, Serialize)] +pub struct PaymentsRequest { + pub source: PaymentSource, + pub amount: i64, + pub currency: String, + pub processing_channel_id: Secret, + #[serde(rename = "3ds")] + pub three_ds: CheckoutThreeDS, + #[serde(flatten)] + pub return_url: ReturnUrl, + pub capture: bool, + pub reference: String, } ``` -`Source` is an enum type. Request types will need to derive `Serialize` and response types will need to derive `Deserialize`. For request types `From` needs to be implemented. +Since Router is connector agnostic, only minimal data is sent to connector and optional fields may be ignored. + +Here processing_channel_id, is specific to checkout and implementations of such functions should be inside the checkout directory. +Let's define `PaymentSource` + +`PaymentSource` is an enum type. Request types will need to derive `Serialize` and response types will need to derive `Deserialize`. For request types `From` needs to be implemented. +For request types that involve an amount, the implementation of `TryFrom<&ConnectorRouterData<&T>>` is required: + +```rust +impl TryFrom<&CheckoutRouterData<&T>> for PaymentsRequest +``` +else ```rust -impl<'a> From<&types::RouterData<'a>> for CheckoutRequestType +impl TryFrom for PaymentsRequest ``` +where `T` is a generic type which can be `types::PaymentsAuthorizeRouterData`, `types::PaymentsCaptureRouterData`, etc. + In this impl block we build the request type from RouterData which will almost always contain all the required information you need for payment processing. `RouterData` contains all the information required for processing the payment. @@ -165,39 +176,56 @@ While implementing the Response Type, the important Enum to be defined for every It stores the different status types that the connector can give in its response that is listed in its API spec. Below is the definition for checkout ```rust -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Default, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] pub enum CheckoutPaymentStatus { Authorized, + #[default] Pending, #[serde(rename = "Card Verified")] CardVerified, Declined, + Captured, } ``` The important part is mapping it to the Router status codes. ```rust -impl From for enums::AttemptStatus { - fn from(item: CheckoutPaymentStatus) -> Self { - match item { - CheckoutPaymentStatus::Authorized => enums::AttemptStatus::Charged, - CheckoutPaymentStatus::Declined => enums::AttemptStatus::Failure, - CheckoutPaymentStatus::Pending => enums::AttemptStatus::Authorizing, - CheckoutPaymentStatus::CardVerified => enums::AttemptStatus::Pending, +impl ForeignFrom<(CheckoutPaymentStatus, Option)> for enums::AttemptStatus { + fn foreign_from(item: (CheckoutPaymentStatus, Option)) -> Self { + let (status, balances) = item; + + match status { + CheckoutPaymentStatus::Authorized => { + if let Some(Balances { + available_to_capture: 0, + }) = balances + { + Self::Charged + } else { + Self::Authorized + } + } + CheckoutPaymentStatus::Captured => Self::Charged, + CheckoutPaymentStatus::Declined => Self::Failure, + CheckoutPaymentStatus::Pending => Self::AuthenticationPending, + CheckoutPaymentStatus::CardVerified => Self::Pending, } } } ``` +If you're converting ConnectorPaymentStatus to AttemptStatus without any additional conditions, you can employ the `impl From for enums::AttemptStatus`. -Note: `enum::AttemptStatus` is Router status. +Note: A payment intent can have multiple payment attempts. `enums::AttemptStatus` represents the status of a payment attempt. -Router status are given below +Some of the attempt status are given below -- **Charged :** The amount has been debited -- **PendingVBV :** Pending but verified by visa -- **Failure :** The payment Failed -- **Authorizing :** In the process of authorizing. +- **Charged :** The payment attempt has succeeded. +- **Pending :** Payment is in processing state. +- **Failure :** The payment attempt has failed. +- **Authorized :** Payment is authorized. Authorized payment can be voided, captured and partial captured. +- **AuthenticationPending :** Customer action is required. +- **Voided :** The payment was voided and never captured; the funds were returned to the customer. It is highly recommended that the default status is Pending. Only explicit failure and explicit success from the connector shall be marked as success or failure respectively. @@ -213,26 +241,119 @@ impl Default for CheckoutPaymentStatus { Below is rest of the response type implementation for checkout ```rust - -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct CheckoutPaymentsResponse { +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct PaymentsResponse { id: String, - amount: i64, + amount: Option, + action_id: Option, status: CheckoutPaymentStatus, + #[serde(rename = "_links")] + links: Links, + balances: Option, + reference: Option, + response_code: Option, + response_summary: Option, } -impl<'a> From> for types::RouterData<'a> { - fn from(item: types::ResponseRouterData<'a, CheckoutPaymentsResponse>) -> Self { - types::RouterData { - connector_transaction_id: Some(item.response.id), - amount_received: Some(item.response.amount), - status: enums::Status::from(item.response.status), +#[derive(Deserialize, Debug)] +pub struct ActionResponse { + #[serde(rename = "id")] + pub action_id: String, + pub amount: i64, + #[serde(rename = "type")] + pub action_type: ActionType, + pub approved: Option, + pub reference: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum PaymentsResponseEnum { + ActionResponse(Vec), + PaymentResponse(Box), +} + +impl TryFrom> + for types::PaymentsAuthorizeRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsResponseRouterData, + ) -> Result { + let redirection_data = item.response.links.redirect.map(|href| { + services::RedirectForm::from((href.redirection_url, services::Method::Get)) + }); + let status = enums::AttemptStatus::foreign_from(( + item.response.status, + item.data.request.capture_method, + )); + let error_response = if status == enums::AttemptStatus::Failure { + Some(types::ErrorResponse { + status_code: item.http_code, + code: item + .response + .response_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: item + .response + .response_summary + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: item.response.response_summary, + attempt_status: None, + connector_transaction_id: None, + }) + } else { + None + }; + let payments_response_data = types::PaymentsResponseData::TransactionResponse { + 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.reference.unwrap_or(item.response.id), + ), + }; + Ok(Self { + status, + response: error_response.map_or_else(|| Ok(payments_response_data), Err), ..item.data - } + }) } } ``` +Using an enum for a response struct in Rust is not recommended due to potential deserialization issues where the deserializer attempts to deserialize into all the enum variants. A preferable alternative is to employ a separate enum for the possible response variants and include it as a field within the response struct. + +Some recommended fields that needs to be set on connector request and response + +- **connector_request_reference_id :** Most of the connectors anticipate merchants to include their own reference ID in payment requests. For instance, the merchant's reference ID in the checkout `PaymentRequest` is specified as `reference`. + +```rust + reference: item.router_data.connector_request_reference_id.clone(), +``` +- **connector_response_reference_id :** Merchants might face ambiguity when deciding which ID to use in the connector dashboard for payment identification. It is essential to populate the connector_response_reference_id with the appropriate reference ID, allowing merchants to recognize the transaction. This field can be linked to either `merchant_reference` or `connector_transaction_id`, depending on the field that the connector dashboard search functionality supports. + +```rust + connector_response_reference_id: item.response.reference.or(Some(item.response.id)) +``` + +- **resource_id :** The connector assigns an identifier to a payment attempt, referred to as `connector_transaction_id`. This identifier is represented as an enum variant for the `resource_id`. If the connector does not provide a `connector_transaction_id`, the resource_id is set to `NoResponseId`. + +```rust + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), +``` +- **redirection_data :** For the implementation of a redirection flow (3D Secure, bank redirects, etc.), assign the redirection link to the `redirection_data`. + +```rust + let redirection_data = item.response.links.redirect.map(|href| { + services::RedirectForm::from((href.redirection_url, services::Method::Get)) + }); +``` + + And finally the error type implementation ```rust @@ -251,23 +372,250 @@ Similarly for every API endpoint you can implement request and response types. The `mod.rs` file contains the trait implementations where we use the types in transformers. -There are four types of tasks that are done by implementing traits: - -- **Payment :** For making/initiating payments -- **PaymentSync :** For checking status of the payment -- **Refund :** For initiating refund -- **RefundSync :** For checking status of the Refund. - We create a struct with the connector name and have trait implementations for it. The following trait implementations are mandatory -- **ConnectorCommon :** contains common description of the connector, like the base endpoint, content-type, error message, id. -- **Payment :** Trait Relationship, has impl block. -- **PaymentAuthorize :** Trait Relationship, has impl block. -- **ConnectorIntegration :** For every api endpoint contains the url, using request transform and response transform and headers. -- **Refund :** Trait Relationship, has empty body. -- **RefundExecute :** Trait Relationship, has empty body. -- **RefundSync :** Trait Relationship, has empty body. +**ConnectorCommon :** contains common description of the connector, like the base endpoint, content-type, error response handling, id, currency unit. + +Within the `ConnectorCommon` trait, you'll find the following methods : + + - `id` method corresponds directly to the connector name. + ```rust + fn id(&self) -> &'static str { + "checkout" + } + ``` + - `get_currency_unit` method anticipates you to [specify the accepted currency unit](#set-the-currency-unit) for the connector. + ```rust + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + ``` + - `common_get_content_type` method requires you to provide the accepted content type for the connector API. + ```rust + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + ``` + - `get_auth_header` method accepts common HTTP Authorization headers that are accepted in all `ConnectorIntegration` flows. + ```rust + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth: checkout::CheckoutAuthType = auth_type + .try_into() + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", auth.api_secret.peek()).into_masked(), + )]) + } + ``` + + - `base_url` method is for fetching the base URL of connector's API. Base url needs to be consumed from configs. + ```rust + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.checkout.base_url.as_ref() + } + ``` + - `build_error_response` method is common error response handling for a connector if it is same in all cases + + ```rust + fn build_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: checkout::ErrorResponse = if res.response.is_empty() { + let (error_codes, error_type) = if res.status_code == 401 { + ( + Some(vec!["Invalid api key".to_string()]), + Some("invalid_api_key".to_string()), + ) + } else { + (None, None) + }; + checkout::ErrorResponse { + request_id: None, + error_codes, + error_type, + } + } else { + res.response + .parse_struct("ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)? + }; + + router_env::logger::info!(error_response=?response); + let errors_list = response.error_codes.clone().unwrap_or_default(); + let option_error_code_message = conn_utils::get_error_code_error_message_based_on_priority( + self.clone(), + errors_list + .into_iter() + .map(|errors| errors.into()) + .collect(), + ); + Ok(types::ErrorResponse { + status_code: res.status_code, + code: option_error_code_message + .clone() + .map(|error_code_message| error_code_message.error_code) + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: option_error_code_message + .map(|error_code_message| error_code_message.error_message) + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: response + .error_codes + .map(|errors| errors.join(" & ")) + .or(response.error_type), + attempt_status: None, + connector_transaction_id: None, + }) + } + ``` + +**ConnectorIntegration :** For every api endpoint contains the url, using request transform and response transform and headers. +Within the `ConnectorIntegration` trait, you'll find the following methods implemented(below mentioned is example for authorized flow): + +- `get_url` method defines endpoint for authorize flow, base url is consumed from `ConnectorCommon` trait. + +```rust + fn get_url( + &self, + _req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "payments")) + } +``` +- `get_headers` method accepts HTTP headers that are accepted for authorize flow. In this context, it is utilized from the `ConnectorCommonExt` trait, as the connector adheres to common headers across various flows. + +```rust + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } +``` + +- `get_request_body` method calls transformers where hyperswitch payment request data is transformed into connector payment request. For constructing the request body have a function `log_and_get_request_body` that allows generic argument which is the struct that is passed as the body for connector integration, and a function that can be use to encode it into String. We log the request in this function, as the struct will be intact and the masked values will be masked. + +```rust + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = checkout::CheckoutRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = checkout::PaymentsRequest::try_from(&connector_router_data)?; + let checkout_req = types::RequestBody::log_and_get_request_body( + &connector_req, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(checkout_req)) + } +``` + +- `build_request` method assembles the API request by providing the method, URL, headers, and request body as parameters. +```rust + fn build_request( + &self, + req: &types::RouterData< + api::Authorize, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } +``` +- `handle_response` method calls transformers where connector response data is transformed into hyperswitch response. +```rust + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: types::Response, + ) -> CustomResult { + let response: checkout::PaymentsResponse = res + .response + .parse_struct("PaymentIntentResponse") + .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) + } +``` +- `get_error_response` method to manage error responses. As the handling of checkout errors remains consistent across various flows, we've incorporated it from the `build_error_response` method within the `ConnectorCommon` trait. +```rust + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +``` +**ConnectorCommonExt :** An enhanced trait for `ConnectorCommon` that enables functions with a generic type. This trait includes the `build_headers` method, responsible for constructing both the common headers and the Authorization headers (retrieved from the `get_auth_header` method), returning them as a vector. + +```rust + where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let 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) + } +} +``` + +**Payment :** This trait includes several other traits and is meant to represent the functionality related to payments. + +**PaymentAuthorize :** This trait extends the `api::ConnectorIntegration `trait with specific types related to payment authorization. + +**PaymentCapture :** This trait extends the `api::ConnectorIntegration `trait with specific types related to manual payment capture. + +**PaymentSync :** This trait extends the `api::ConnectorIntegration `trait with specific types related to payment retrieve. + +**Refund :** This trait includes several other traits and is meant to represent the functionality related to Refunds. + +**RefundExecute :** This trait extends the `api::ConnectorIntegration `trait with specific types related to refunds create. + +**RefundSync :** This trait extends the `api::ConnectorIntegration `trait with specific types related to refunds retrieve. + And the below derive traits @@ -277,13 +625,105 @@ And the below derive traits There is a trait bound to implement refunds, if you don't want to implement refunds you can mark them as `todo!()` but code panics when you initiate refunds then. -Don’t forget to add logs lines in appropriate places. Refer to other connector code for trait implementations. Mostly the rust compiler will guide you to do it easily. Feel free to connect with us in case of any queries and if you want to confirm the status mapping. +### **Set the currency Unit** +The `get_currency_unit` function, part of the ConnectorCommon trait, enables connectors to specify their accepted currency unit as either `Base` or `Minor`. For instance, Paypal designates its currency in the base unit (for example, USD), whereas Hyperswitch processes amounts in the minor unit (for example, cents). If a connector accepts amounts in the base unit, conversion is required, as illustrated. + +``` rust +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for PaypalRouterData +{ + type Error = error_stack::Report; + fn try_from( + (currency_unit, currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data: item, + }) + } +} +``` + +**Note:** Since the amount is being converted in the aforementioned `try_from`, it is necessary to retrieve amounts from `ConnectorRouterData` in all other `try_from` instances. + +### **Connector utility functions** + +In the `connector/utils.rs` file, you'll discover utility functions that aid in constructing connector requests and responses. We highly recommend using these helper functions for retrieving payment request fields, such as `get_billing_country`, `get_browser_info`, and `get_expiry_date_as_yyyymm`, as well as for validations, including `is_three_ds`, `is_auto_capture`, and more. + +```rust + let json_wallet_data: CheckoutGooglePayData =wallet_data.get_wallet_token_as_json()?; +``` + ### **Test the connector** -Try running the tests in `crates/router/tests/connectors/{{connector-name}}.rs`. +The template code script generates a test file for the connector, containing 20 sanity tests. We anticipate that you will implement these tests when adding a new connector. + +```rust +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} +``` + +Utility functions for tests are also available at `tests/connector/utils`. These functions enable you to write tests with ease. + +```rust + /// For initiating payments when `CaptureMethod` is set to `Manual` + /// This doesn't complete the transaction, `PaymentsCapture` needs to be done manually + async fn authorize_payment( + &self, + payment_data: Option, + payment_info: Option, + ) -> Result> { + let integration = self.get_data().connector.get_connector_integration(); + let mut request = self.generate_data( + types::PaymentsAuthorizeData { + confirm: true, + capture_method: Some(diesel_models::enums::CaptureMethod::Manual), + ..(payment_data.unwrap_or(PaymentAuthorizeType::default().0)) + }, + payment_info, + ); + let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( + Settings::new().unwrap(), + StorageImpl::PostgresqlTest, + tx, + Box::new(services::MockApiClient), + ) + .await; + integration.execute_pretasks(&mut request, &state).await?; + Box::pin(call_connector(request, integration)).await + } +``` + +Prior to executing tests in the shell, ensure that the API keys are configured in `crates/router/tests/connectors/sample_auth.toml` and set the environment variable `CONNECTOR_AUTH_FILE_PATH` using the export command. Avoid pushing code with exposed API keys. + +```rust + export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_auth.toml" + cargo test --package router --test connectors -- checkout --test-threads=1 +``` All tests should pass and add appropriate tests for connector specific payment flows. ### **Build payment request and response from json schema** diff --git a/config/config.example.toml b/config/config.example.toml index 02eff1d42979..87999f0e9e93 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -21,37 +21,52 @@ 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] host = "127.0.0.1" port = 6379 -pool_size = 5 # Number of connections to keep open -reconnect_max_attempts = 5 # Maximum number of reconnection attempts to make before failing. Set to 0 to retry forever. -reconnect_delay = 5 # Delay between reconnection attempts, in milliseconds -default_ttl = 300 # Default TTL for entries, in seconds -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 +pool_size = 5 # Number of connections to keep open +reconnect_max_attempts = 5 # Maximum number of reconnection attempts to make before failing. Set to 0 to retry forever. +reconnect_delay = 5 # Delay between reconnection attempts, in milliseconds +default_ttl = 300 # Default TTL for entries, in seconds +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 +auto_pipeline = true # Whether or not the client should automatically pipeline commands across tasks when possible. +disable_auto_backpressure = false # Whether or not to disable the automatic backpressure features when pipelining is enabled. +max_in_flight_commands = 5000 # The maximum number of in-flight commands (per connection) before backpressure will be applied. +default_command_timeout = 0 # An optional timeout to apply to all commands. +max_feed_count = 200 # The maximum number of frames that will be fed to a socket before flushing. + +# 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 20000 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. @@ -95,27 +110,27 @@ 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 +host = "" # Locker host +host_rs = "" # Rust Locker host +mock_locker = true # Emulate a locker locally using Postgres +locker_signing_key_id = "1" # Key_id to sign basilisk hs locker +locker_enabled = true # Boolean to enable or disable saving cards in locker [delayed_session_response] connectors_with_delayed_session_response = "trustpay,payme" # List of connectors which has delayed session response @@ -124,16 +139,9 @@ 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_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 - +vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs # Refund configuration [refund] @@ -201,10 +209,13 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" @@ -234,11 +245,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] @@ -287,6 +298,12 @@ lower_fetch_limit = 1800 # Lower limit for fetching entries from redis lock_key = "PRODUCER_LOCKING_KEY" # The following keys defines the producer lock that is created in redis with lock_ttl = 160 # the ttl being the expiry (in seconds) +# Scheduler server configuration +[scheduler.server] +port = 3000 # Port on which the server will listen for incoming requests +host = "127.0.0.1" # Host IP address to bind the server to +workers = 1 # Number of actix workers to handle incoming requests concurrently + batch_size = 200 # Specifies the batch size the producer will push under a single entry in the redis queue # Drainer configuration, which handles draining raw SQL queries from Redis streams to the SQL database @@ -312,90 +329,108 @@ 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"} +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } +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" } +bankofamerica = { payment_method = "card" } +cybersource = { payment_method = "card" } +nmi = { payment_method = "card" } +payme = {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,cybersource" } # 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 +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} # Mandate supported payment method type and connector for bank_redirect +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} +wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon" } + # 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,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +seven_eleven = { country = "JP", currency = "JPY" } +lawson = { country = "JP", currency = "JPY" } +mini_stop = { country = "JP", currency = "JPY" } +family_mart = { country = "JP", currency = "JPY" } +seicomart = { country = "JP", currency = "JPY" } +pay_easy = { country = "JP", currency = "JPY" } [pm_filters.zen] credit = { not_available_flows = { capture_method = "manual" } } @@ -414,18 +449,19 @@ credit = { currency = "USD" } debit = { currency = "USD" } ach = { currency = "USD" } -[pm_filters.stripe] -cashapp = {country = "US", currency = "USD"} - [pm_filters.prophetpay] card_redirect = { currency = "USD" } +[pm_filters.helcim] +credit = { currency = "USD" } +debit = { currency = "USD" } + [connector_customer] connector_list = "gocardless,stax,stripe" payout_connector_list = "wise" [bank_config.online_banking_fpx] -adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" [bank_config.online_banking_thailand] adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" @@ -434,18 +470,45 @@ 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 generated by Elliptic-curve prime256v1 curve. You can use `openssl ecparam -out private.key -name prime256v1 -genkey` to generate the private key +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 generated by RSA:2048 algorithm. Refer Hyperswitch Docs (https://docs.hyperswitch.io/hyperswitch-cloud/payment-methods-setup/wallets/apple-pay/ios-application/) to generate the private key + +[applepay_merchant_configs] +# Run below command to get common merchant identifier for applepay in shell +# +# CERT_PATH="path/to/certificate.pem" +# MERCHANT_ID=$(openssl x509 -in "$CERT_PATH" -noout -text | +# awk -v oid="1.2.840.113635.100.6.32" ' +# BEGIN { RS = "\n\n" } +# /X509v3 extensions/ { in_extension=1 } +# in_extension && /'"$oid"'/ { print $0; exit }' | +# grep -oE '\.@[A-F0-9]+' | sed 's/^\.@//' +# ) +# echo "Merchant ID: $MERCHANT_ID" +common_merchant_identifier = "APPLE_PAY_COMMON_MERCHANT_IDENTIFIER" # This can be obtained by decrypting the apple_pay_ppc_key as shown above in comments +merchant_cert = "APPLE_PAY_MERCHANT_CERTIFICATE" # Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate +merchant_cert_key = "APPLE_PAY_MERCHANT_CERTIFICATE_KEY" # Private key generate by RSA:2048 algorithm. Refer Hyperswitch Docs (https://docs.hyperswitch.io/hyperswitch-cloud/payment-methods-setup/wallets/apple-pay/ios-application/) to generate the private key +applepay_endpoint = "https://apple-pay-gateway.apple.com/paymentservices/registerMerchant" # Apple pay gateway merchant endpoint [payment_link] -sdk_url = "http://localhost:9090/dist/HyperLoader.js" +sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" + +[payment_method_auth] +redis_expiry = 900 +pm_auth_key = "Some_pm_auth_key" # Analytics configuration. [analytics] source = "sqlx" # The Analytics source/strategy to be used +[analytics.clickhouse] +username = "" # Clickhouse username +password = "" # Clickhouse password (optional) +host = "" # Clickhouse host in http(s)://: format +database_name = "" # Clickhouse database name + [analytics.sqlx] username = "db_user" # Analytics DB Username password = "db_pass" # Analytics DB Password @@ -454,8 +517,38 @@ port = 5432 # Analytics 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 # Config for KV setup [kv_config] # TTL for KV in seconds ttl = 900 + +[frm] +enabled = true + +[paypal_onboarding] +client_id = "paypal_client_id" # Client ID for PayPal onboarding +client_secret = "paypal_secret_key" # Secret key for PayPal onboarding +partner_id = "paypal_partner_id" # Partner ID for PayPal onboarding +enabled = true # Switch to enable or disable PayPal onboarding + +[events] +source = "logs" # The event sink to push events supports kafka or logs (stdout) + +[events.kafka] +brokers = [] # Kafka broker urls for bootstrapping the client +intent_analytics_topic = "topic" # Kafka topic to be used for PaymentIntent events +attempt_analytics_topic = "topic" # Kafka topic to be used for PaymentAttempt events +refund_analytics_topic = "topic" # Kafka topic to be used for Refund events +api_logs_topic = "topic" # Kafka topic to be used for incoming api events +connector_logs_topic = "topic" # Kafka topic to be used for connector api events +outgoing_webhook_logs_topic = "topic" # Kafka topic to be used for outgoing webhook events + +# File storage configuration +[file_storage] +file_storage_backend = "aws_s3" # File storage backend to be used + +[file_storage.aws_s3] +region = "us-east-1" # The AWS region used by the AWS S3 for file storage +bucket_name = "bucket1" # The AWS S3 bucket name for file storage diff --git a/config/deployments/README.md b/config/deployments/README.md new file mode 100644 index 000000000000..c807892f1e06 --- /dev/null +++ b/config/deployments/README.md @@ -0,0 +1,158 @@ +# Configs for deployments + +## Introduction + +This directory contains the configs for deployments of Hyperswitch in different hosted environments. + +Hyperswitch has **3** components namely, + +- router +- drainer +- scheduler + - consumer + - producer + +We maintain configs for the `router` component for 3 different environments, namely, + +- Integration Test +- Sandbox +- Production + +To learn about what "router", "drainer" and "scheduler" is, please refer to the [Hyperswitch architecture][architecture] documentation. + +### Tree structure + +```text +config/deployments # Root directory for the deployment configs +├── README.md # This file +├── drainer.toml # Config specific to drainer +├── env_specific.toml # Config for environment specific values which are meant to be sensitive (to be set by the user) +├── integration_test.toml # Config specific to integration_test environment +├── production.toml # Config specific to production environment +├── sandbox.toml # Config specific to sandbox environment +└── scheduler # Directory for scheduler configs + ├── consumer.toml # Config specific to consumer + └── producer.toml # Config specific to producer +``` + +## Router + +The `integration_test.toml`, `sandbox.toml`, and `production.toml` files are configuration files for the environments `integration_test`, `sandbox`, and `production`, respectively. These files maintain a 1:1 mapping with the environment names, and it is recommended to use the same name for the environment throughout this document. + +### Generating a Config File for the Router + +The `env_specific.toml` file contains values that are specific to the environment. This file is kept separate because the values in it are sensitive and are meant to be set by the user. The `env_specific.toml` file is merged with the `integration_test.toml`, `sandbox.toml`, or `production.toml` file to create the final configuration file for the router. + +For example, to build and deploy Hyperswitch in the **sandbox environment**, you can duplicate the `env_specific.toml` file and rename it as `sandbox_config.toml`. Then, update the values in the file with the proper values for the sandbox environment. + +The environment-specific `sandbox.toml` file, which contains the Hyperswitch recommended defaults, is merged with the `sandbox_config.toml` file to create the final configuration file called `sandbox_release.toml`. This file is marked as ready for deploying on the sandbox environment. + +1. Duplicate the `env_specific.toml` file and rename it as `sandbox_config.toml`: + + ```shell + cp config/deployments/env_specific.toml config/deployments/sandbox_config.toml + ``` + +2. Update the values in the `sandbox_config.toml` file with the proper values for the sandbox environment: + + ```shell + vi config/deployments/sandbox_config.toml + ``` + +3. To merge the files you can use `cat`: + + ```shell + cat config/deployments/sandbox.toml config/deployments/sandbox_config.toml > config/deployments/sandbox_release.toml + ``` + +> [!NOTE] +> You can refer to the [`config.example.toml`][config_example] file to understand the variables that used are in the `env_specific.toml` file. + +## Scheduler + +The scheduler has two components, namely `consumer` and `producer`. + +The `consumer.toml` and `producer.toml` files are the configuration files for the `consumer` and `producer`, respectively. These files contain the default values recommended by Hyperswitch. + +### Generating a Config File for the Scheduler + +Scheduler configuration files are built on top of the router configuration files. So, the `sandbox_release.toml` file is merged with the `consumer.toml` or `producer.toml` file to create the final configuration file for the scheduler. + +You can use `cat` to merge the files in the terminal. + +- Below is an example for consumer in sandbox environment: + + ```shell + cat config/deployments/scheduler/consumer.toml config/deployments/sandbox_release.toml > config/deployments/consumer_sandbox_release.toml + ``` + +- Below is an example for producer in sandbox environment: + + ```shell + cat config/deployments/scheduler/producer.toml config/deployments/sandbox_release.toml > config/deployments/producer_sandbox_release.toml + ``` + +## Drainer + +Drainer is an independent component, and hence, the drainer configs can be used directly provided that the user updates the `drainer.toml` file with proper values before using. + +## Running Hyperswitch through Docker Compose + +To run the router, you can use the following snippet in the `docker-compose.yml` file: + +```yaml +### Application services +hyperswitch-server: + image: juspaydotin/hyperswitch-router:latest # This pulls the latest image from Docker Hub. If you wish to use a version without added features (like KMS), you can replace `latest` with `standalone`. However, please note that the standalone version is not recommended for production use. + command: /local/bin/router --config-path /local/config/deployments/sandbox_release.toml # <--- Change this to the config file that is generated for the environment. + ports: + - "8080:8080" + volumes: + - ./config:/local/config +``` + +To run the producer, you can use the following snippet in the `docker-compose.yml` file: + +```yaml +hyperswitch-producer: + image: juspaydotin/hyperswitch-producer:latest + command: /local/bin/scheduler --config-path /local/config/deployments/producer_sandbox_release.toml # <--- Change this to the config file that is generated for the environment. + volumes: + - ./config:/local/config + environment: + - SCHEDULER_FLOW=producer +``` + +To run the consumer, you can use the following snippet in the `docker-compose.yml` file: + +```yaml +hyperswitch-consumer: + image: juspaydotin/hyperswitch-consumer:latest + command: /local/bin/scheduler --config-path /local/config/deployments/consumer_sandbox_release.toml # <--- Change this to the config file that is generated for the environment + volumes: + - ./config:/local/config + environment: + - SCHEDULER_FLOW=consumer +``` + +To run the drainer, you can use the following snippet in the `docker-compose.yml` file: + +```yaml +hyperswitch-drainer: + image: juspaydotin/hyperswitch-drainer:latest + command: /local/bin/drainer --config-path /local/config/deployments/drainer.toml + volumes: + - ./config:/local/config +``` + +> [!NOTE] +> You can replace the term `sandbox` with the environment name that you are deploying to (e.g., `production`, `integration_test`, etc.) with respective changes (optional) and use the same steps to generate the final configuration file for the environment. + +You can verify that the server is up and running by hitting the health check endpoint: + +```shell +curl --head --request GET 'http://localhost:8080/health' +``` + +[architecture]: /docs/architecture.md +[config_example]: /config/config.example.toml diff --git a/config/deployments/drainer.toml b/config/deployments/drainer.toml new file mode 100644 index 000000000000..42c89cbfd584 --- /dev/null +++ b/config/deployments/drainer.toml @@ -0,0 +1,39 @@ +[drainer] +loop_interval = 500 +max_read_count = 100 +num_partitions = 64 +shutdown_interval = 1000 +stream_name = "drainer_stream" + +[kms] +key_id = "kms_key_id" +region = "kms_region" + +[log.console] +enabled = true +level = "DEBUG" +log_format = "json" + +[log.telemetry] +metrics_enabled = true +otel_exporter_otlp_endpoint = "http://localhost:4317" + +[master_database] +dbname = "master_database_name" +host = "localhost" +password = "master_database_password" +pool_size = 3 +port = 5432 +username = "username" + +[redis] +cluster_enabled = false +cluster_urls = ["redis.cluster.uri-1:8080", "redis.cluster.uri-2:4115"] # List of redis cluster urls +default_ttl = 300 +host = "localhost" +pool_size = 5 +port = 6379 +reconnect_delay = 5 +reconnect_max_attempts = 5 +stream_read_count = 1 +use_legacy_version = false diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml new file mode 100644 index 000000000000..04831376050d --- /dev/null +++ b/config/deployments/env_specific.toml @@ -0,0 +1,215 @@ +# For explanantion of each config, please refer to the `config/config.example.toml` file + +[analytics.clickhouse] +username = "clickhouse_username" # Clickhouse username +password = "clickhouse_password" # Clickhouse password (optional) +host = "http://localhost:8123" # Clickhouse host in http(s)://: format +database_name = "clickhouse_db_name" # Clickhouse database name + +# Analytics configuration. +[analytics] +source = "sqlx" # The Analytics source/strategy to be used + +[analytics.sqlx] +username = "db_user" # Analytics DB Username +password = "db_pass" # Analytics DB Password +host = "localhost" # Analytics DB Host +port = 5432 # Analytics 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 + +[api_keys] +kms_encrypted_hash_key = "base64_encoded_ciphertext" # Base64-encoded (KMS encrypted) ciphertext of the API key hashing key +hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" # API key hashing key. Only applicable when KMS is disabled. + +[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 generated by Elliptic-curve prime256v1 curve. You can use `openssl ecparam -out private.key -name prime256v1 -genkey` to generate the private key +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 generated by RSA:2048 algorithm. Refer Hyperswitch Docs (https://docs.hyperswitch.io/hyperswitch-cloud/payment-methods-setup/wallets/apple-pay/ios-application/) to generate the private key + +[applepay_merchant_configs] +common_merchant_identifier = "APPLE_PAY_COMMON_MERCHANT_IDENTIFIER" # Refer to config.example.toml to learn how you can generate this value +merchant_cert = "APPLE_PAY_MERCHANT_CERTIFICATE" # Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate +merchant_cert_key = "APPLE_PAY_MERCHANT_CERTIFICATE_KEY" # Private key generate by RSA:2048 algorithm. Refer Hyperswitch Docs (https://docs.hyperswitch.io/hyperswitch-cloud/payment-methods-setup/wallets/apple-pay/ios-application/) to generate the private key +applepay_endpoint = "https://apple-pay-gateway.apple.com/paymentservices/registerMerchant" # Apple pay gateway merchant endpoint + +[connector_onboarding.paypal] +enabled = true # boolean +client_id = "paypal_client_id" +client_secret = "paypal_client_secret" +partner_id = "paypal_partner_id" + +[connector_request_reference_id_config] +merchant_ids_send_payment_id_as_connector_request_id = ["merchant_id_1", "merchant_id_2", "etc.,"] + +# EmailClient configuration. Only applicable when the `email` feature flag is enabled. +[email] +sender_email = "example@example.com" # Sender email +aws_region = "" # AWS region used by AWS SES +base_url = "" # Dashboard base url used when adding links that should redirect to self, say https://app.hyperswitch.io for example +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. + +[events] +source = "logs" # The event sink to push events supports kafka or logs (stdout) + +[events.kafka] +brokers = [] # Kafka broker urls for bootstrapping the client +intent_analytics_topic = "topic" # Kafka topic to be used for PaymentIntent events +attempt_analytics_topic = "topic" # Kafka topic to be used for PaymentAttempt events +refund_analytics_topic = "topic" # Kafka topic to be used for Refund events +api_logs_topic = "topic" # Kafka topic to be used for incoming api events +connector_logs_topic = "topic" # Kafka topic to be used for connector api events +outgoing_webhook_logs_topic = "topic" # Kafka topic to be used for outgoing webhook events + +# File storage configuration +[file_storage] +file_storage_backend = "aws_s3" # File storage backend to be used + +[file_storage.aws_s3] +region = "bucket_region" # The AWS region used by AWS S3 for file storage +bucket_name = "bucket" # The AWS S3 bucket name for file storage + +# 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 20000 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 + +[jwekey] # 3 priv/pub key pair +vault_encryption_key = "" # public key in pem format, corresponding private key in rust locker +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 rust locker + +# KMS configuration. Only applicable when the `kms` feature flag is enabled. +[kms] +key_id = "" # The AWS key ID used by the KMS SDK for decrypting data. +region = "" # The AWS region used by the KMS SDK for decrypting data. + +# 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 +locker_signing_key_id = "1" # Key_id to sign basilisk hs locker +locker_enabled = true # Boolean to enable or disable saving cards in locker +redis_temp_locker_encryption_key = "redis_temp_locker_encryption_key" # Encryption key for redis temp locker + +[log.console] +enabled = true +level = "DEBUG" +log_format = "json" + +[log.file] +enabled = false +level = "DEBUG" +log_format = "json" + +# Telemetry configuration for metrics and traces +[log.telemetry] +traces_enabled = false # boolean [true or false], whether traces are enabled +metrics_enabled = false # boolean [true or false], whether metrics are enabled +ignore_errors = false # boolean [true or false], whether to ignore errors during traces or metrics pipeline setup +sampling_rate = 0.1 # decimal rate between 0.0 - 1.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"] + +[lock_settings] +delay_between_retries_in_milliseconds = 500 # Delay between retries in milliseconds +redis_lock_expiry_seconds = 180 # Seconds before the redis lock expires + +# 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 + +[payment_link] +sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" + +[payment_method_auth] +pm_auth_key = "pm_auth_key" # Payment method auth key used for authorization +redis_expiry = 900 # Redis expiry time in milliseconds + +[proxy] +http_url = "http://proxy_http_url" # Outgoing proxy http URL to proxy the HTTP traffic +https_url = "https://proxy_https_url" # Outgoing proxy https URL to proxy the HTTPS traffic + +# Redis credentials +[redis] +host = "127.0.0.1" +port = 6379 +pool_size = 5 # Number of connections to keep open +reconnect_max_attempts = 5 # Maximum number of reconnection attempts to make before failing. Set to 0 to retry forever. +reconnect_delay = 5 # Delay between reconnection attempts, in milliseconds +default_ttl = 300 # Default TTL for entries, in seconds +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 +auto_pipeline = true # Whether or not the client should automatically pipeline commands across tasks when possible. +disable_auto_backpressure = false # Whether or not to disable the automatic backpressure features when pipelining is enabled. +max_in_flight_commands = 5000 # The maximum number of in-flight commands (per connection) before backpressure will be applied. +default_command_timeout = 0 # An optional timeout to apply to all commands. +max_feed_count = 200 # The maximum number of frames that will be fed to a socket before flushing. +cluster_enabled = true # boolean +cluster_urls = ["redis.cluster.uri-1:8080", "redis.cluster.uri-2:4115"] # List of redis cluster urls + +# 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 + +[report_download_config] +dispute_function = "report_download_config_dispute_function" # Config to download dispute report +payment_function = "report_download_config_payment_function" # Config to download payment report +refund_function = "report_download_config_refund_function" # Config to download refund report +region = "report_download_config_region" # Region of the bucket + +# 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 + +# Server configuration +[server] +base_url = "https://server_base_url" +workers = 8 +port = 8080 +host = "127.0.0.1" +# This is the grace time (in seconds) given to the actix-server to stop the execution +# For more details: https://actix.rs/docs/server/#graceful-shutdown +shutdown_timeout = 30 +# HTTP Request body limit. Defaults to 32kB +request_body_limit = 32_768 diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml new file mode 100644 index 000000000000..2c5d16e3e3c7 --- /dev/null +++ b/config/deployments/integration_test.toml @@ -0,0 +1,281 @@ +[bank_config] +eps.adyen.banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" +eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" +ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" +online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +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" +online_banking_slovakia.adyen.banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo" +online_banking_thailand.adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" +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" +przelewy24.stripe.banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,banki_spbdzielcze,blik,bnp_paribas,boz,citi,credit_agricole,e_transfer_pocztowy24,getin_bank,idea_bank,inteligo,mbank_mtransfer,nest_przelew,noble_pay,pbac_z_ipko,plus_bank,santander_przelew24,toyota_bank,volkswagen_bank" + +[connectors] +aci.base_url = "https://eu-test.oppwa.com/" +adyen.base_url = "https://checkout-test.adyen.com/" +adyen.secondary_base_url = "https://pal-test.adyen.com/" +airwallex.base_url = "https://api-demo.airwallex.com/" +applepay.base_url = "https://apple-pay-gateway.apple.com/" +authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" +bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" +bitpay.base_url = "https://test.bitpay.com" +bluesnap.base_url = "https://sandbox.bluesnap.com/" +bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" +boku.base_url = "https://$-api4-stage.boku.com" +braintree.base_url = "https://api.sandbox.braintreegateway.com/" +braintree.secondary_base_url = "https://payments.sandbox.braintree-api.com/graphql" +cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" +checkout.base_url = "https://api.sandbox.checkout.com/" +coinbase.base_url = "https://api.commerce.coinbase.com" +cryptopay.base_url = "https://business-sandbox.cryptopay.me" +cybersource.base_url = "https://apitest.cybersource.com/" +dlocal.base_url = "https://sandbox.dlocal.com/" +dummyconnector.base_url = "http://localhost:8080/dummy-connector" +fiserv.base_url = "https://cert.api.fiservapps.com/" +forte.base_url = "https://sandbox.forte.net/api/v3" +globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" +globepay.base_url = "https://pay.globepay.co/" +gocardless.base_url = "https://api-sandbox.gocardless.com" +helcim.base_url = "https://api.helcim.com/" +iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" +klarna.base_url = "https://api-na.playground.klarna.com/" +mollie.base_url = "https://api.mollie.com/v2/" +mollie.secondary_base_url = "https://api.cc.mollie.com/v1/" +multisafepay.base_url = "https://testapi.multisafepay.com/" +nexinets.base_url = "https://apitest.payengine.de/v1" +nmi.base_url = "https://secure.nmi.com/" +noon.base_url = "https://api-test.noonpayments.com/" +noon.key_mode = "Test" +nuvei.base_url = "https://ppp-test.nuvei.com/" +opayo.base_url = "https://pi-test.sagepay.com/" +opennode.base_url = "https://dev-api.opennode.com" +payeezy.base_url = "https://api-cert.payeezy.com/" +payme.base_url = "https://sandbox.payme.io/" +paypal.base_url = "https://api-m.sandbox.paypal.com/" +payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" +powertranz.base_url = "https://staging.ptranz.com/api/" +prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" +rapyd.base_url = "https://sandboxapi.rapyd.net" +shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" +riskified.base_url = "https://sandbox.riskified.com/api" +square.base_url = "https://connect.squareupsandbox.com/" +square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" +stax.base_url = "https://apiprod.fattlabs.com/" +stripe.base_url = "https://api.stripe.com/" +stripe.base_url_file_upload = "https://files.stripe.com/" +trustpay.base_url = "https://test-tpgw.trustpay.eu/" +trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" +tsys.base_url = "https://stagegw.transnox.com/" +volt.base_url = "https://api.sandbox.volt.io/" +wise.base_url = "https://api.sandbox.transferwise.tech/" +worldline.base_url = "https://eu.sandbox.api-ingenico.com/" +worldpay.base_url = "https://try.access.worldpay.com/" +zen.base_url = "https://api.zen-test.com/" +zen.secondary_base_url = "https://secure.zen-test.com/" + +[dummy_connector] +enabled = true +assets_base_url = "https://app.hyperswitch.io/assets/TestProcessor/" +authorize_ttl = 36000 +default_return_url = "https://app.hyperswitch.io/" +discord_invite_url = "https://discord.gg/wJZ7DVW8mm" +payment_complete_duration = 500 +payment_complete_tolerance = 100 +payment_duration = 1000 +payment_retrieve_duration = 500 +payment_retrieve_tolerance = 100 +payment_tolerance = 100 +payment_ttl = 172800 +refund_duration = 1000 +refund_retrieve_duration = 500 +refund_retrieve_tolerance = 100 +refund_tolerance = 100 +refund_ttl = 172800 +slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-1k6cz4lee-SAJzhz6bjmpp4jZCDOtOIg" + +[frm] +enabled = true + +[connector_customer] +connector_list = "gocardless,stax,stripe" +payout_connector_list = "wise" + +[delayed_session_response] +connectors_with_delayed_session_response = "trustpay,payme" + +[mandates.supported_payment_methods] +bank_debit.ach.connector_list = "gocardless" +bank_debit.becs.connector_list = "gocardless" +bank_debit.sepa.connector_list = "gocardless" +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon" +wallet.google_pay.connector_list = "stripe,adyen,cybersource" +wallet.paypal.connector_list = "adyen" +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} + +[multiple_api_version_supported_connectors] +supported_connectors = "braintree" + +[payouts] +payout_eligibility = true + +[pm_filters.default] +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "GBP" } +apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA", currency = "AED,AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AT,BE,DK,FI,FR,DE,IE,IT,NL,NO,ES,SE,GB,US,CA", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +paypal.currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } + +[pm_filters.adyen] +ach = { country = "US", currency = "USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } +alfamart = { country = "ID", currency = "IDR" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +ali_pay_hk = { country = "HK", currency = "HKD" } +alma = { country = "FR", currency = "EUR" } +apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,GB,SE,NO,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +bacs = { country = "GB", currency = "GBP" } +bancontact_card = { country = "BE", currency = "EUR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bizum = { country = "ES", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +bni_va = { country = "ID", currency = "IDR" } +boleto = { country = "BR", currency = "BRL" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +dana = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +eps = { country = "AT", currency = "EUR" } +family_mart = { country = "JP", currency = "JPY" } +gcash = { country = "PH", currency = "PHP" } +giropay = { country = "DE", currency = "EUR" } +go_pay = { country = "ID", currency = "IDR" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +ideal = { country = "NL", currency = "EUR" } +indomaret = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +lawson = { country = "JP", currency = "JPY" } +mandiri_va = { country = "ID", currency = "IDR" } +mb_way = { country = "PT", currency = "EUR" } +mini_stop = { country = "JP", currency = "JPY" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +momo = { country = "VN", currency = "VND" } +momo_atm = { country = "VN", currency = "VND" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +online_banking_thailand = { country = "TH", currency = "THB" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_bright = { country = "CA", currency = "CAD" } +pay_easy = { country = "JP", currency = "JPY" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +seicomart = { country = "JP", currency = "JPY" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +seven_eleven = { country = "JP", currency = "JPY" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +swish = { country = "SE", currency = "SEK" } +touch_n_go = { country = "MY", currency = "MYR" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +twint = { country = "CH", currency = "CHF" } +vipps = { country = "NO", currency = "NOK" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD,CNY" } + +[pm_filters.authorizedotnet] +google_pay.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" +paypal.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" + +[pm_filters.braintree] +paypal.currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" + +[pm_filters.forte] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.helcim] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.globepay] +ali_pay.currency = "GBP,CNY" +we_chat_pay.currency = "GBP,CNY" + +[pm_filters.klarna] +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } + +[pm_filters.prophetpay] +card_redirect.currency = "USD" + +[pm_filters.stax] +ach = { country = "US", currency = "USD" } + +[pm_filters.stripe] +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD" } +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA" +cashapp = { country = "US", currency = "USD" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,CAD,CHF,CZK,DKK,EUR,GBP,NOK,NZD,PLN,SEK,USD" } +sofort = { country = "AT,BE,DE,IT,NL,ES", currency = "EUR" } + +[pm_filters.worldpay] +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US" +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" + +[pm_filters.zen] +boleto = { country = "BR", currency = "BRL" } +efecty = { country = "CO", currency = "COP" } +multibanco = { country = "PT", currency = "EUR" } +pago_efectivo = { country = "PE", currency = "PEN" } +pix = { country = "BR", currency = "BRL" } +pse = { country = "CO", currency = "COP" } +red_compra = { country = "CL", currency = "CLP" } +red_pagos = { country = "UY", currency = "UYU" } + +[temp_locker_enable_config] +bluesnap.payment_method = "card" +nuvei.payment_method = "card" +shift4.payment_method = "card" +stripe.payment_method = "bank_transfer" +bankofamerica = { payment_method = "card" } +cybersource = { payment_method = "card" } +nmi.payment_method = "card" + +[tokenization] +braintree = { long_lived_token = false, payment_method = "card" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } +gocardless = { long_lived_token = true, payment_method = "bank_debit" } +mollie = { long_lived_token = false, payment_method = "card" } +payme = { long_lived_token = false, payment_method = "card" } +square = { long_lived_token = false, payment_method = "card" } +stax = { long_lived_token = true, payment_method = "card,bank_debit" } +stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { list = "google_pay", type = "disable_only" } } + +[webhooks] +outgoing_enabled = true + +[webhook_source_verification_call] +connectors_with_webhook_source_verification_call = "paypal" diff --git a/config/deployments/production.toml b/config/deployments/production.toml new file mode 100644 index 000000000000..964281c52bba --- /dev/null +++ b/config/deployments/production.toml @@ -0,0 +1,297 @@ +[bank_config] +eps.adyen.banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" +eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" +ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" +online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +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" +online_banking_slovakia.adyen.banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" +online_banking_thailand.adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" +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" +przelewy24.stripe.banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,banki_spbdzielcze,blik,bnp_paribas,boz,citi,credit_agricole,e_transfer_pocztowy24,getin_bank,idea_bank,inteligo,mbank_mtransfer,nest_przelew,noble_pay,pbac_z_ipko,plus_bank,santander_przelew24,toyota_bank,volkswagen_bank" + +[connector_customer] +connector_list = "stax,stripe,gocardless" +payout_connector_list = "wise" + +[connectors] +aci.base_url = "https://eu-test.oppwa.com/" +adyen.base_url = "https://checkout-test.adyen.com/" +adyen.secondary_base_url = "https://pal-test.adyen.com/" +airwallex.base_url = "https://api-demo.airwallex.com/" +applepay.base_url = "https://apple-pay-gateway.apple.com/" +authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" +bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://api.merchant-services.bankofamerica.com/" +bitpay.base_url = "https://bitpay.com" +bluesnap.base_url = "https://ws.bluesnap.com/" +bluesnap.secondary_base_url = "https://pay.bluesnap.com/" +boku.base_url = "https://country-api4-stage.boku.com" +braintree.base_url = "https://api.sandbox.braintreegateway.com/" +braintree.secondary_base_url = "https://payments.braintree-api.com/graphql" +cashtocode.base_url = "https://cluster14.api.cashtocode.com" +checkout.base_url = "https://api.checkout.com/" +coinbase.base_url = "https://api.commerce.coinbase.com" +cryptopay.base_url = "https://business.cryptopay.me/" +cybersource.base_url = "https://api.cybersource.com/" +dlocal.base_url = "https://sandbox.dlocal.com/" +dummyconnector.base_url = "http://localhost:8080/dummy-connector" +fiserv.base_url = "https://cert.api.fiservapps.com/" +forte.base_url = "https://sandbox.forte.net/api/v3" +globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" +globepay.base_url = "https://pay.globepay.co/" +gocardless.base_url = "https://api.gocardless.com" +helcim.base_url = "https://api.helcim.com/" +iatapay.base_url = "https://iata-pay.iata.org/api/v1" +klarna.base_url = "https://api-na.playground.klarna.com/" +mollie.base_url = "https://api.mollie.com/v2/" +mollie.secondary_base_url = "https://api.cc.mollie.com/v1/" +multisafepay.base_url = "https://testapi.multisafepay.com/" +nexinets.base_url = "https://api.payengine.de/v1" +nmi.base_url = "https://secure.nmi.com/" +noon.base_url = "https://api.noonpayments.com/" +noon.key_mode = "Live" +nuvei.base_url = "https://ppp-test.nuvei.com/" +opayo.base_url = "https://pi-live.sagepay.com/" +opennode.base_url = "https://api.opennode.com" +payeezy.base_url = "https://api.payeezy.com/" +payme.base_url = "https://live.payme.io/" +paypal.base_url = "https://api-m.paypal.com/" +payu.base_url = "https://secure.payu.com/api/" +placetopay.base_url = "https://checkout.placetopay.com/rest/gateway" +powertranz.base_url = "https://staging.ptranz.com/api/" +prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" +rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://wh.riskified.com/api/" +shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" +square.base_url = "https://connect.squareupsandbox.com/" +square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" +stax.base_url = "https://apiprod.fattlabs.com/" +stripe.base_url = "https://api.stripe.com/" +stripe.base_url_file_upload = "https://files.stripe.com/" +trustpay.base_url = "https://tpgw.trustpay.eu/" +trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" +tsys.base_url = "https://gateway.transit-pass.com/" +volt.base_url = "https://api.volt.io/" +wise.base_url = "https://api.sandbox.transferwise.tech/" +worldline.base_url = "https://eu.sandbox.api-ingenico.com/" +worldpay.base_url = "https://try.access.worldpay.com/" +zen.base_url = "https://api.zen.com/" +zen.secondary_base_url = "https://secure.zen.com/" + +[delayed_session_response] +connectors_with_delayed_session_response = "trustpay,payme" + +[dummy_connector] +assets_base_url = "https://app.hyperswitch.io/assets/TestProcessor/" +authorize_ttl = 36000 +default_return_url = "https://app.hyperswitch.io/" +discord_invite_url = "https://discord.gg/wJZ7DVW8mm" +enabled = false +payment_complete_duration = 500 +payment_complete_tolerance = 100 +payment_duration = 1000 +payment_retrieve_duration = 500 +payment_retrieve_tolerance = 100 +payment_tolerance = 100 +payment_ttl = 172800 +refund_duration = 1000 +refund_retrieve_duration = 500 +refund_retrieve_tolerance = 100 +refund_tolerance = 100 +refund_ttl = 172800 +slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-1k6cz4lee-SAJzhz6bjmpp4jZCDOtOIg" + +[frm] +enabled = false + +[mandates.supported_payment_methods] +bank_debit.ach.connector_list = "gocardless" +bank_debit.becs.connector_list = "gocardless" +bank_debit.sepa.connector_list = "gocardless" +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon" +wallet.google_pay.connector_list = "stripe,adyen,cybersource" +wallet.paypal.connector_list = "adyen" +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} + +[multiple_api_version_supported_connectors] +supported_connectors = "braintree" + +[payouts] +payout_eligibility = true + +[pm_filters.default] +ach = { country = "US", currency = "USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD,CNY" } +apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +bacs = { country = "GB", currency = "GBP" } +bancontact_card = { country = "BE", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +mb_way = { country = "PT", currency = "EUR" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +pay_bright = { country = "CA", currency = "CAD" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } + +[pm_filters.adyen] +ach = { country = "US", currency = "USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,CA,ES,FR,IT,NZ,GB,US", currency = "USD,AUD,CAD,NZD,GBP" } +alfamart = { country = "ID", currency = "IDR" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +ali_pay_hk = { country = "HK", currency = "HKD" } +alma = { country = "FR", currency = "EUR" } +apple_pay = { country = "AE,AM,AR,AT,AU,AZ,BE,BG,BH,BR,BY,CA,CH,CN,CO,CR,CY,CZ,DE,DK,EE,ES,FI,FO,FR,GB,GE,GG,GL,GR,HK,HR,HU,IE,IL,IM,IS,IT,JE,JO,JP,KW,KZ,LI,LT,LU,LV,MC,MD,ME,MO,MT,MX,MY,NL,NO,NZ,PE,PL,PS,PT,QA,RO,RS,SA,SE,SG,SI,SK,SM,TW,UA,GB,UM,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +bacs = { country = "GB", currency = "GBP" } +bancontact_card = { country = "BE", currency = "EUR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bizum = { country = "ES", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +bni_va = { country = "ID", currency = "IDR" } +boleto = { country = "BR", currency = "BRL" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +dana = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +eps = { country = "AT", currency = "EUR" } +family_mart = { country = "JP", currency = "JPY" } +gcash = { country = "PH", currency = "PHP" } +giropay = { country = "DE", currency = "EUR" } +go_pay = { country = "ID", currency = "IDR" } +google_pay = { country = "AE,AG,AL,AO,AR,AS,AT,AU,AZ,BE,BG,BH,BR,BY,CA,CH,CL,CO,CY,CZ,DE,DK,DO,DZ,EE,EG,ES,FI,FR,GB,GR,HK,HR,HU,ID,IE,IL,IN,IS,IT,JO,JP,KE,KW,KZ,LB,LI,LK,LT,LU,LV,MT,MX,MY,NL,NO,NZ,OM,PA,PE,PH,PK,PL,PT,QA,RO,RU,SA,SE,SG,SI,SK,TH,TR,TW,UA,GB,US,UY,VN,ZA", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +ideal = { country = "NL", currency = "EUR" } +indomaret = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +klarna = { country = "AT,BE,CA,CH,DE,DK,ES,FI,FR,GB,IE,IT,NL,NO,PL,PT,SE,GB,US", currency = "AUD,CAD,CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD" } +lawson = { country = "JP", currency = "JPY" } +mandiri_va = { country = "ID", currency = "IDR" } +mb_way = { country = "PT", currency = "EUR" } +mini_stop = { country = "JP", currency = "JPY" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +momo = { country = "VN", currency = "VND" } +momo_atm = { country = "VN", currency = "VND" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +online_banking_thailand = { country = "TH", currency = "THB" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_bright = { country = "CA", currency = "CAD" } +pay_easy = { country = "JP", currency = "JPY" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +seicomart = { country = "JP", currency = "JPY" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +seven_eleven = { country = "JP", currency = "JPY" } +sofort = { country = "AT,BE,CH,DE,ES,FI,FR,GB,IT,NL,PL,SE,GB", currency = "EUR" } +swish = { country = "SE", currency = "SEK" } +touch_n_go = { country = "MY", currency = "MYR" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +twint = { country = "CH", currency = "CHF" } +vipps = { country = "NO", currency = "NOK" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } + +[pm_filters.authorizedotnet] +google_pay.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" +paypal.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" + +[pm_filters.braintree] +paypal.currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" + +[pm_filters.forte] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.helcim] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.globepay] +ali_pay.currency = "GBP,CNY" +we_chat_pay.currency = "GBP,CNY" + +[pm_filters.klarna] +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } + +[pm_filters.prophetpay] +card_redirect.currency = "USD" + +[pm_filters.stax] +ach = { country = "US", currency = "USD" } + +[pm_filters.stripe] +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD" } +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA" +cashapp = { country = "US", currency = "USD" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,CAD,CHF,CZK,DKK,EUR,GBP,NOK,NZD,PLN,SEK,USD" } +sofort = { country = "AT,BE,DE,IT,NL,ES", currency = "EUR" } + +[pm_filters.worldpay] +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US" +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" + +[pm_filters.zen] +boleto = { country = "BR", currency = "BRL" } +efecty = { country = "CO", currency = "COP" } +multibanco = { country = "PT", currency = "EUR" } +pago_efectivo = { country = "PE", currency = "PEN" } +pix = { country = "BR", currency = "BRL" } +pse = { country = "CO", currency = "COP" } +red_compra = { country = "CL", currency = "CLP" } +red_pagos = { country = "UY", currency = "UYU" } + +[temp_locker_enable_config] +bluesnap.payment_method = "card" +nuvei.payment_method = "card" +shift4.payment_method = "card" +stripe.payment_method = "bank_transfer" +bankofamerica = { payment_method = "card" } +cybersource = { payment_method = "card" } +nmi.payment_method = "card" + +[tokenization] +braintree = { long_lived_token = false, payment_method = "card" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } +gocardless = { long_lived_token = true, payment_method = "bank_debit" } +mollie = { long_lived_token = false, payment_method = "card" } +payme = { long_lived_token = false, payment_method = "card" } +square = { long_lived_token = false, payment_method = "card" } +stax = { long_lived_token = true, payment_method = "card,bank_debit" } +stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { list = "google_pay", type = "disable_only" } } + +[webhooks] +outgoing_enabled = true + +[webhook_source_verification_call] +connectors_with_webhook_source_verification_call = "paypal" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml new file mode 100644 index 000000000000..aa2377cf8a08 --- /dev/null +++ b/config/deployments/sandbox.toml @@ -0,0 +1,299 @@ +[bank_config] +eps.adyen.banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" +eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" +ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" +online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +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" +online_banking_slovakia.adyen.banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo" +online_banking_thailand.adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" +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" +przelewy24.stripe.banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,banki_spbdzielcze,blik,bnp_paribas,boz,citi,credit_agricole,e_transfer_pocztowy24,getin_bank,idea_bank,inteligo,mbank_mtransfer,nest_przelew,noble_pay,pbac_z_ipko,plus_bank,santander_przelew24,toyota_bank,volkswagen_bank" + +[connector_customer] +connector_list = "stax,stripe,gocardless" +payout_connector_list = "wise" + +[connectors] +aci.base_url = "https://eu-test.oppwa.com/" +adyen.base_url = "https://checkout-test.adyen.com/" +adyen.secondary_base_url = "https://pal-test.adyen.com/" +airwallex.base_url = "https://api-demo.airwallex.com/" +applepay.base_url = "https://apple-pay-gateway.apple.com/" +authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" +bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" +bitpay.base_url = "https://test.bitpay.com" +bluesnap.base_url = "https://sandbox.bluesnap.com/" +bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" +boku.base_url = "https://$-api4-stage.boku.com" +braintree.base_url = "https://api.sandbox.braintreegateway.com/" +braintree.secondary_base_url = "https://payments.sandbox.braintree-api.com/graphql" +cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" +checkout.base_url = "https://api.sandbox.checkout.com/" +coinbase.base_url = "https://api.commerce.coinbase.com" +cryptopay.base_url = "https://business-sandbox.cryptopay.me" +cybersource.base_url = "https://apitest.cybersource.com/" +dlocal.base_url = "https://sandbox.dlocal.com/" +dummyconnector.base_url = "http://localhost:8080/dummy-connector" +fiserv.base_url = "https://cert.api.fiservapps.com/" +forte.base_url = "https://sandbox.forte.net/api/v3" +globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" +globepay.base_url = "https://pay.globepay.co/" +gocardless.base_url = "https://api-sandbox.gocardless.com" +helcim.base_url = "https://api.helcim.com/" +iatapay.base_url = "https://iata-pay.iata.org/api/v1" +klarna.base_url = "https://api-na.playground.klarna.com/" +mollie.base_url = "https://api.mollie.com/v2/" +mollie.secondary_base_url = "https://api.cc.mollie.com/v1/" +multisafepay.base_url = "https://testapi.multisafepay.com/" +nexinets.base_url = "https://apitest.payengine.de/v1" +nmi.base_url = "https://secure.nmi.com/" +noon.base_url = "https://api-test.noonpayments.com/" +noon.key_mode = "Test" +nuvei.base_url = "https://ppp-test.nuvei.com/" +opayo.base_url = "https://pi-test.sagepay.com/" +opennode.base_url = "https://dev-api.opennode.com" +payeezy.base_url = "https://api-cert.payeezy.com/" +payme.base_url = "https://sandbox.payme.io/" +paypal.base_url = "https://api-m.sandbox.paypal.com/" +payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" +powertranz.base_url = "https://staging.ptranz.com/api/" +prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" +rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" +shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" +square.base_url = "https://connect.squareupsandbox.com/" +square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" +stax.base_url = "https://apiprod.fattlabs.com/" +stripe.base_url = "https://api.stripe.com/" +stripe.base_url_file_upload = "https://files.stripe.com/" +trustpay.base_url = "https://test-tpgw.trustpay.eu/" +trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" +tsys.base_url = "https://stagegw.transnox.com/" +volt.base_url = "https://api.sandbox.volt.io/" +wise.base_url = "https://api.sandbox.transferwise.tech/" +worldline.base_url = "https://eu.sandbox.api-ingenico.com/" +worldpay.base_url = "https://try.access.worldpay.com/" +zen.base_url = "https://api.zen-test.com/" +zen.secondary_base_url = "https://secure.zen-test.com/" + +[delayed_session_response] +connectors_with_delayed_session_response = "trustpay,payme" + +[dummy_connector] +enabled = true +assets_base_url = "https://app.hyperswitch.io/assets/TestProcessor/" +authorize_ttl = 36000 +default_return_url = "https://app.hyperswitch.io/" +discord_invite_url = "https://discord.gg/wJZ7DVW8mm" +payment_complete_duration = 500 +payment_complete_tolerance = 100 +payment_duration = 1000 +payment_retrieve_duration = 500 +payment_retrieve_tolerance = 100 +payment_tolerance = 100 +payment_ttl = 172800 +refund_duration = 1000 +refund_retrieve_duration = 500 +refund_retrieve_tolerance = 100 +refund_tolerance = 100 +refund_ttl = 172800 +slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-1k6cz4lee-SAJzhz6bjmpp4jZCDOtOIg" + +[frm] +enabled = true + +[mandates.supported_payment_methods] +bank_debit.ach.connector_list = "gocardless" +bank_debit.becs.connector_list = "gocardless" +bank_debit.sepa.connector_list = "gocardless" +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon" +wallet.google_pay.connector_list = "stripe,adyen,cybersource" +wallet.paypal.connector_list = "adyen" +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} + +[multiple_api_version_supported_connectors] +supported_connectors = "braintree" + +[payouts] +payout_eligibility = true + +[pm_filters.default] +ach = { country = "US", currency = "USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA", currency = "AED,AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +bacs = { country = "GB", currency = "GBP" } +bancontact_card = { country = "BE", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +mb_way = { country = "PT", currency = "EUR" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +pay_bright = { country = "CA", currency = "CAD" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } + +[pm_filters.adyen] +ach = { country = "US", currency = "USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } +alfamart = { country = "ID", currency = "IDR" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +ali_pay_hk = { country = "HK", currency = "HKD" } +alma = { country = "FR", currency = "EUR" } +apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,GB,SE,NO,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +bacs = { country = "GB", currency = "GBP" } +bancontact_card = { country = "BE", currency = "EUR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bizum = { country = "ES", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +bni_va = { country = "ID", currency = "IDR" } +boleto = { country = "BR", currency = "BRL" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +dana = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +eps = { country = "AT", currency = "EUR" } +family_mart = { country = "JP", currency = "JPY" } +gcash = { country = "PH", currency = "PHP" } +giropay = { country = "DE", currency = "EUR" } +go_pay = { country = "ID", currency = "IDR" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +ideal = { country = "NL", currency = "EUR" } +indomaret = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +lawson = { country = "JP", currency = "JPY" } +mandiri_va = { country = "ID", currency = "IDR" } +mb_way = { country = "PT", currency = "EUR" } +mini_stop = { country = "JP", currency = "JPY" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +momo = { country = "VN", currency = "VND" } +momo_atm = { country = "VN", currency = "VND" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +online_banking_thailand = { country = "TH", currency = "THB" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_bright = { country = "CA", currency = "CAD" } +pay_easy = { country = "JP", currency = "JPY" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +seicomart = { country = "JP", currency = "JPY" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +seven_eleven = { country = "JP", currency = "JPY" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +swish = { country = "SE", currency = "SEK" } +touch_n_go = { country = "MY", currency = "MYR" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +twint = { country = "CH", currency = "CHF" } +vipps = { country = "NO", currency = "NOK" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +pix = { country = "BR", currency = "BRL" } + +[pm_filters.authorizedotnet] +google_pay.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" +paypal.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" + +[pm_filters.braintree] +paypal.currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" + +[pm_filters.forte] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.helcim] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.globepay] +ali_pay.currency = "GBP,CNY" +we_chat_pay.currency = "GBP,CNY" + +[pm_filters.klarna] +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } + +[pm_filters.prophetpay] +card_redirect.currency = "USD" + +[pm_filters.stax] +ach = { country = "US", currency = "USD" } + +[pm_filters.stripe] +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD" } +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA" +cashapp = { country = "US", currency = "USD" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,CAD,CHF,CZK,DKK,EUR,GBP,NOK,NZD,PLN,SEK,USD" } +sofort = { country = "AT,BE,DE,IT,NL,ES", currency = "EUR" } + +[pm_filters.worldpay] +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US" +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" + +[pm_filters.zen] +boleto = { country = "BR", currency = "BRL" } +efecty = { country = "CO", currency = "COP" } +multibanco = { country = "PT", currency = "EUR" } +pago_efectivo = { country = "PE", currency = "PEN" } +pix = { country = "BR", currency = "BRL" } +pse = { country = "CO", currency = "COP" } +red_compra = { country = "CL", currency = "CLP" } +red_pagos = { country = "UY", currency = "UYU" } + + +[temp_locker_enable_config] +bluesnap.payment_method = "card" +nuvei.payment_method = "card" +shift4.payment_method = "card" +stripe.payment_method = "bank_transfer" +bankofamerica = { payment_method = "card" } +cybersource = { payment_method = "card" } +nmi.payment_method = "card" + +[tokenization] +braintree = { long_lived_token = false, payment_method = "card" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } +gocardless = { long_lived_token = true, payment_method = "bank_debit" } +mollie = { long_lived_token = false, payment_method = "card" } +payme = { long_lived_token = false, payment_method = "card" } +square = { long_lived_token = false, payment_method = "card" } +stax = { long_lived_token = true, payment_method = "card,bank_debit" } +stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { list = "google_pay", type = "disable_only" } } + +[webhooks] +outgoing_enabled = true + +[webhook_source_verification_call] +connectors_with_webhook_source_verification_call = "paypal" diff --git a/config/deployments/scheduler/consumer.toml b/config/deployments/scheduler/consumer.toml new file mode 100644 index 000000000000..cdd605526689 --- /dev/null +++ b/config/deployments/scheduler/consumer.toml @@ -0,0 +1,17 @@ +# Scheduler settings provides a point to modify the behaviour of scheduler flow. +# It defines the the streams/queues name and configuration as well as event selection variables +[scheduler] +consumer_group = "scheduler_group" +graceful_shutdown_interval = 60000 # Specifies how much time to wait while re-attempting shutdown for a service (in milliseconds) +loop_interval = 3000 # Specifies how much time to wait before starting the defined behaviour of producer or consumer (in milliseconds)0 +stream = "scheduler_stream" + +[scheduler.consumer] +consumer_group = "scheduler_group" +disabled = false # This flag decides if the consumer should actively consume task + +# Scheduler server configuration +[scheduler.server] +port = 3000 # Port on which the server will listen for incoming requests +host = "127.0.0.1" # Host IP address to bind the server to +workers = 1 # Number of actix workers to handle incoming requests concurrently diff --git a/config/deployments/scheduler/producer.toml b/config/deployments/scheduler/producer.toml new file mode 100644 index 000000000000..9cbaee96f03c --- /dev/null +++ b/config/deployments/scheduler/producer.toml @@ -0,0 +1,20 @@ +# Scheduler settings provides a point to modify the behaviour of scheduler flow. +# It defines the the streams/queues name and configuration as well as event selection variables +[scheduler] +consumer_group = "scheduler_group" +graceful_shutdown_interval = 60000 # Specifies how much time to wait while re-attempting shutdown for a service (in milliseconds) +loop_interval = 30000 # Specifies how much time to wait before starting the defined behaviour of producer or consumer (in milliseconds) +stream = "scheduler_stream" + +[scheduler.producer] +batch_size = 50 # Specifies the batch size the producer will push under a single entry in the redis queue +lock_key = "producer_locking_key" # The following keys defines the producer lock that is created in redis with +lock_ttl = 160 # the ttl being the expiry (in seconds) +lower_fetch_limit = 900 # Lower limit for fetching entries from redis queue (in seconds) +upper_fetch_limit = 0 # Upper limit for fetching entries from the redis queue (in seconds)0 + +# Scheduler server configuration +[scheduler.server] +port = 3000 # Port on which the server will listen for incoming requests +host = "127.0.0.1" # Host IP address to bind the server to +workers = 1 # Number of actix workers to handle incoming requests concurrently diff --git a/config/development.toml b/config/development.toml index c82607a704c3..20abb7bd6f30 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" @@ -30,6 +31,23 @@ dbname = "hyperswitch_db" pool_size = 5 connection_timeout = 10 +[redis] +host = "127.0.0.1" +port = 6379 +pool_size = 5 +reconnect_max_attempts = 5 +reconnect_delay = 5 +default_ttl = 300 +default_hash_ttl = 900 +use_legacy_version = false +stream_read_count = 1 +auto_pipeline = true +disable_auto_backpressure = false +max_in_flight_commands = 5000 +default_command_timeout = 0 +max_feed_count = 200 + + [server] # HTTP Request body limit. Defaults to 32kB request_body_limit = 32768 @@ -51,14 +69,19 @@ host = "" host_rs = "" mock_locker = true basilisk_host = "" +locker_enabled = true + + +[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 = "" -locker_encryption_key1 = "" -locker_encryption_key2 = "" -locker_decryption_key1 = "" -locker_decryption_key2 = "" vault_encryption_key = "" rust_locker_encryption_key = "" vault_private_key = "" @@ -103,6 +126,7 @@ cards = [ "payme", "paypal", "payu", + "placetopay", "powertranz", "prophetpay", "shift4", @@ -175,10 +199,13 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" @@ -201,10 +228,21 @@ stream = "SCHEDULER_STREAM" disabled = false consumer_group = "SCHEDULER_GROUP" +[scheduler.server] +port = 3000 +host = "127.0.0.1" +workers = 1 + [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" } @@ -230,7 +268,7 @@ stripe = { banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,ba 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_config.online_banking_fpx] -adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" [bank_config.online_banking_thailand] adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" @@ -260,31 +298,31 @@ ideal = { country = "NL", currency = "EUR" } cashapp = { country = "US", currency = "USD" } [pm_filters.adyen] -google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } -apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,UK,SE,NO,AK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } -paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,GB,SE,NO,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } -ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } -we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } mb_way = { country = "PT", currency = "EUR" } -klarna = { country = "AT,ES,UK,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } affirm = { country = "US", currency = "USD" } -afterpay_clearpay = { country = "AU,NZ,ES,UK,FR,IT,CA,US", currency = "GBP" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } pay_bright = { country = "CA", currency = "CAD" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } giropay = { country = "DE", currency = "EUR" } eps = { country = "AT", currency = "EUR" } -sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } ideal = { country = "NL", currency = "EUR" } blik = {country = "PL", currency = "PLN"} -trustly = {country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK"} +trustly = {country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK"} online_banking_czech_republic = {country = "CZ", currency = "EUR,CZK"} online_banking_finland = {country = "FI", currency = "EUR"} online_banking_poland = {country = "PL", currency = "PLN"} online_banking_slovakia = {country = "SK", currency = "EUR,CZK"} bancontact_card = {country = "BE", currency = "EUR"} ach = {country = "US", currency = "USD"} -bacs = {country = "UK", currency = "GBP"} +bacs = {country = "GB", currency = "GBP"} sepa = {country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR"} ali_pay_hk = {country = "HK", currency = "HKD"} bizum = {country = "ES", currency = "EUR"} @@ -308,19 +346,24 @@ 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"} +pay_safe_card = {country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU"} seven_eleven = {country = "JP", currency = "JPY"} lawson = {country = "JP", currency = "JPY"} mini_stop = {country = "JP", currency = "JPY"} family_mart = {country = "JP", currency = "JPY"} seicomart = {country = "JP", currency = "JPY"} pay_easy = {country = "JP", currency = "JPY"} +pix = { country = "BR", currency = "BRL" } [pm_filters.braintree] paypal = { currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" } credit = { not_available_flows = { capture_method = "manual" } } debit = { not_available_flows = { capture_method = "manual" } } +[pm_filters.helcim] +credit = { currency = "USD" } +debit = { currency = "USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,EUR,EUR,CAD,CZK,DKK,EUR,EUR,EUR,EUR,EUR,EUR,EUR,NZD,NOK,PLN,EUR,EUR,SEK,CHF,GBP,USD" } credit = { not_available_flows = { capture_method = "manual" } } @@ -380,7 +423,7 @@ debit = { currency = "USD" } [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" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } mollie = {long_lived_token = false, payment_method = "card"} square = {long_lived_token = false, payment_method = "card"} @@ -393,6 +436,10 @@ stripe = {payment_method = "bank_transfer"} nuvei = {payment_method = "card"} shift4 = {payment_method = "card"} bluesnap = {payment_method = "card"} +bankofamerica = {payment_method = "card"} +cybersource = {payment_method = "card"} +nmi = {payment_method = "card"} +payme = {payment_method = "card"} [connector_customer] connector_list = "gocardless,stax,stripe" @@ -426,14 +473,16 @@ connectors_with_webhook_source_verification_call = "paypal" [mandates.supported_payment_methods] pay_later.klarna = { connector_list = "adyen" } -wallet.google_pay = { connector_list = "stripe,adyen" } -wallet.apple_pay = { connector_list = "stripe,adyen" } +wallet.google_pay = { connector_list = "stripe,adyen,cybersource" } +wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon" } wallet.paypal = { connector_list = "adyen" } -card.credit = { connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } -card.debit = { connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } +card.credit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } +card.debit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} [connector_request_reference_id_config] merchant_ids_send_payment_id_as_connector_request_id = [] @@ -451,7 +500,11 @@ apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" [payment_link] -sdk_url = "http://localhost:9090/dist/HyperLoader.js" +sdk_url = "http://localhost:9050/HyperLoader.js" + +[payment_method_auth] +redis_expiry = 900 +pm_auth_key = "Some_pm_auth_key" [lock_settings] redis_lock_expiry_seconds = 180 # 3 * 60 seconds @@ -459,3 +512,46 @@ delay_between_retries_in_milliseconds = 500 [kv_config] ttl = 900 # 15 * 60 seconds + +[frm] +enabled = true + +[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_logs_topic = "hyperswitch-connector-api-events" +outgoing_webhook_logs_topic = "hyperswitch-outgoing-webhook-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" + +[connector_onboarding.paypal] +client_id = "" +client_secret = "" +partner_id = "" +enabled = true + +[file_storage] +file_storage_backend = "file_system" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index a5294546de41..e6dc01afa741 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" @@ -47,14 +56,9 @@ host = "" host_rs = "" mock_locker = true basilisk_host = "" +locker_enabled = true [jwekey] -locker_key_identifier1 = "" -locker_key_identifier2 = "" -locker_encryption_key1 = "" -locker_encryption_key2 = "" -locker_decryption_key1 = "" -locker_decryption_key2 = "" vault_encryption_key = "" rust_locker_encryption_key = "" vault_private_key = "" @@ -64,6 +68,19 @@ host = "redis-standalone" port = 6379 cluster_enabled = false cluster_urls = ["redis-cluster:6379"] +pool_size = 5 +reconnect_max_attempts = 5 +reconnect_delay = 5 +default_ttl = 300 +default_hash_ttl = 900 +use_legacy_version = false +stream_read_count = 1 +auto_pipeline = true +disable_auto_backpressure = false +max_in_flight_commands = 5000 +default_command_timeout = 0 +max_feed_count = 200 + [refund] max_attempts = 10 @@ -116,10 +133,13 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" @@ -178,6 +198,7 @@ cards = [ "payme", "paypal", "payu", + "placetopay", "powertranz", "prophetpay", "shift4", @@ -206,10 +227,15 @@ stream = "SCHEDULER_STREAM" disabled = false consumer_group = "SCHEDULER_GROUP" +[scheduler.server] +port = 3000 +host = "127.0.0.1" +workers = 1 + #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" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } 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"} @@ -221,6 +247,10 @@ stripe = {payment_method = "bank_transfer"} nuvei = {payment_method = "card"} shift4 = {payment_method = "card"} bluesnap = {payment_method = "card"} +bankofamerica = {payment_method = "card"} +cybersource = {payment_method = "card"} +nmi = {payment_method = "card"} +payme = {payment_method = "card"} [dummy_connector] enabled = true @@ -262,7 +292,7 @@ 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"} +pay_safe_card = {country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU"} seven_eleven = {country = "JP", currency = "JPY"} lawson = {country = "JP", currency = "JPY"} mini_stop = {country = "JP", currency = "JPY"} @@ -288,13 +318,17 @@ cashapp = {country = "US", currency = "USD"} [pm_filters.prophetpay] card_redirect = { currency = "USD" } +[pm_filters.helcim] +credit = { currency = "USD" } +debit = { currency = "USD" } + [pm_filters.stax] credit = { currency = "USD" } debit = { currency = "USD" } ach = { currency = "USD" } [bank_config.online_banking_fpx] -adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" [bank_config.online_banking_thailand] adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" @@ -305,13 +339,15 @@ adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,hal [mandates.supported_payment_methods] pay_later.klarna = {connector_list = "adyen"} wallet.google_pay = {connector_list = "stripe,adyen"} -wallet.apple_pay = {connector_list = "stripe,adyen"} +wallet.apple_pay = {connector_list = "stripe,adyen,cybersource,noon"} wallet.paypal = {connector_list = "adyen"} -card.credit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} -card.debit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} +card.credit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} +card.debit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} [connector_customer] connector_list = "gocardless,stax,stripe" @@ -320,20 +356,56 @@ payout_connector_list = "wise" [multiple_api_version_supported_connectors] supported_connectors = "braintree" +[payment_method_auth] +redis_expiry = 900 +pm_auth_key = "Some_pm_auth_key" + [lock_settings] 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_logs_topic = "hyperswitch-connector-api-events" +outgoing_webhook_logs_topic = "hyperswitch-outgoing-webhook-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 + +[frm] +enabled = true + +[connector_onboarding.paypal] +client_id = "" +client_secret = "" +partner_id = "" +enabled = true + +[events] +source = "logs" + +[file_storage] +file_storage_backend = "file_system" diff --git a/connector-template/mod.rs b/connector-template/mod.rs index 7f21962109de..c64ce431968c 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -15,6 +15,7 @@ use crate::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, + RequestContent } }; @@ -106,6 +107,7 @@ impl ConnectorCommon for {{project-name | downcase | pascal_case}} { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -157,7 +159,7 @@ impl Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) } - fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors,) -> CustomResult, errors::ConnectorError> { + fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors,) -> CustomResult { let connector_router_data = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RouterData::try_from(( &self.get_currency_unit(), @@ -165,10 +167,8 @@ impl req.request.amount, req, ))?; - let req_obj = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest::try_from(&connector_router_data)?; - let {{project-name | downcase}}_req = types::RequestBody::log_and_get_request_body(&req_obj, utils::Encode::<{{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest>::encode_to_string_of_json) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some({{project-name | downcase}}_req)) + let connector_req = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -186,7 +186,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req, connectors)?) + .set_body(types::PaymentsAuthorizeType::get_request_body(self, req, connectors)?) .build(), )) } @@ -303,7 +303,7 @@ impl &self, _req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -320,7 +320,7 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body(self, req, connectors)?) + .set_body(types::PaymentsCaptureType::get_request_body(self, req, connectors)?) .build(), )) } @@ -375,7 +375,7 @@ impl Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) } - fn get_request_body(&self, req: &types::RefundsRouterData, _connectors: &settings::Connectors,) -> CustomResult, errors::ConnectorError> { + fn get_request_body(&self, req: &types::RefundsRouterData, _connectors: &settings::Connectors,) -> CustomResult { let connector_router_data = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RouterData::try_from(( &self.get_currency_unit(), @@ -383,10 +383,8 @@ impl req.request.refund_amount, req, ))?; - let req_obj = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RefundRequest::try_from(&connector_router_data)?; - let {{project-name | downcase}}_req = types::RequestBody::log_and_get_request_body(&req_obj, utils::Encode::<{{project-name | downcase}}::{{project-name | downcase | pascal_case}}RefundRequest>::encode_to_string_of_json) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some({{project-name | downcase}}_req)) + let connector_req = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request(&self, req: &types::RefundsRouterData, connectors: &settings::Connectors,) -> CustomResult,errors::ConnectorError> { @@ -395,7 +393,7 @@ impl .url(&types::RefundExecuteType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundExecuteType::get_headers(self, req, connectors)?) - .body(types::RefundExecuteType::get_request_body(self, req, connectors)?) + .set_body(types::RefundExecuteType::get_request_body(self, req, connectors)?) .build(); Ok(Some(request)) } @@ -443,7 +441,7 @@ impl .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .body(types::RefundSyncType::get_request_body(self, req, connectors)?) + .set_body(types::RefundSyncType::get_request_body(self, req, connectors)?) .build(), )) } @@ -485,7 +483,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..60b13693054d 100644 --- a/connector-template/transformers.rs +++ b/connector-template/transformers.rs @@ -42,7 +42,6 @@ pub struct {{project-name | downcase | pascal_case}}PaymentsRequest { #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct {{project-name | downcase | pascal_case}}Card { - name: Secret, number: cards::CardNumber, expiry_month: Secret, expiry_year: Secret, @@ -56,7 +55,6 @@ impl TryFrom<&{{project-name | downcase | pascal_case}}RouterData<&types::Paymen match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(req_card) => { let card = {{project-name | downcase | pascal_case}}Card { - 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, @@ -130,6 +128,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..ad0fe6d778fd --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql @@ -0,0 +1,242 @@ +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), + `dispute_id` 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), + `dispute_id` 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), + `dispute_id` 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)); +ALTER TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ADD COLUMN `dispute_id` 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), + `dispute_id` 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, + dispute_id +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.sql b/crates/analytics/docs/clickhouse/scripts/api_events.sql new file mode 100644 index 000000000000..49a6472eaa4d --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/api_events.sql @@ -0,0 +1,152 @@ +CREATE TABLE api_events_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), + `error` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at_timestamp` DateTime64(3), + `latency` UInt128, + `user_agent` String, + `ip_addr` String, + `hs_latency` Nullable(UInt128), + `http_method` LowCardinality(String), + `url_path` String, + `dispute_id` Nullable(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_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), + `error` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at_timestamp` DateTime64(3), + `latency` UInt128, + `user_agent` String, + `ip_addr` String, + `hs_latency` Nullable(UInt128), + `http_method` LowCardinality(String), + `url_path` String, + `dispute_id` Nullable(String) + INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, + INDEX apiIndex api_flow TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 +) 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_mv TO 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), + `connector` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_flow` LowCardinality(String), + `api_auth_type` LowCardinality(String), + `request` String, + `response` Nullable(String), + `error` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at_timestamp` DateTime64(3), + `latency` UInt128, + `user_agent` String, + `ip_addr` String, + `hs_latency` Nullable(UInt128), + `http_method` LowCardinality(String), + `url_path` String, + `dispute_id` Nullable(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, + error, + authentication_data, + status_code, + created_at_timestamp, + now() as inserted_at, + latency, + user_agent, + ip_addr, + hs_latency, + http_method, + url_path, + dispute_id +FROM + api_events_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_queue +WHERE length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/scripts/connector_events.sql b/crates/analytics/docs/clickhouse/scripts/connector_events.sql new file mode 100644 index 000000000000..4a53f9edb0bf --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/connector_events.sql @@ -0,0 +1,105 @@ +CREATE TABLE connector_events_queue ( + `merchant_id` String, + `payment_id` Nullable(String), + `connector_name` LowCardinality(String), + `request_id` String, + `flow` LowCardinality(String), + `request` String, + `response` Nullable(String), + `error` Nullable(String), + `status_code` UInt32, + `created_at` DateTime64(3), + `latency` UInt128, + `method` LowCardinality(String), + `refund_id` Nullable(String), + `dispute_id` Nullable(String) +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-connector-api-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE connector_events_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `connector_name` LowCardinality(String), + `request_id` String, + `flow` LowCardinality(String), + `request` String, + `response` Nullable(String), + `error` Nullable(String), + `status_code` UInt32, + `created_at` DateTime64(3), + `inserted_at` DateTime64(3), + `latency` UInt128, + `method` LowCardinality(String), + `refund_id` Nullable(String), + `dispute_id` Nullable(String), + INDEX flowIndex flowTYPE bloom_filter GRANULARITY 1, + INDEX connectorIndex connector_name 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 connector_events_mv TO connector_events_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `connector_name` LowCardinality(String), + `request_id` String, + `flow` LowCardinality(String), + `request` String, + `response` Nullable(String), + `error` Nullable(String), + `status_code` UInt32, + `created_at` DateTime64(3), + `latency` UInt128, + `method` LowCardinality(String), + `refund_id` Nullable(String), + `dispute_id` Nullable(String) +) AS +SELECT + merchant_id, + payment_id, + connector_name, + request_id, + flow, + request, + response, + error, + status_code, + created_at, + now() as inserted_at, + latency, + method, + refund_id, + dispute_id +FROM + connector_events_queue +where length(_error) = 0; + + +CREATE MATERIALIZED VIEW connector_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 connector_events_queue +WHERE length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/scripts/outgoing_webhook_events.sql b/crates/analytics/docs/clickhouse/scripts/outgoing_webhook_events.sql new file mode 100644 index 000000000000..3dc907629d0a --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/outgoing_webhook_events.sql @@ -0,0 +1,109 @@ +CREATE TABLE + outgoing_webhook_events_queue ( + `merchant_id` String, + `event_id` Nullable(String), + `event_type` LowCardinality(String), + `outgoing_webhook_event_type` LowCardinality(String), + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `attempt_id` Nullable(String), + `dispute_id` Nullable(String), + `payment_method_id` Nullable(String), + `mandate_id` Nullable(String), + `content` Nullable(String), + `is_error` Bool, + `error` Nullable(String), + `created_at_timestamp` DateTime64(3) + ) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', + kafka_topic_list = 'hyperswitch-outgoing-webhook-events', + kafka_group_name = 'hyper-c1', + kafka_format = 'JSONEachRow', + kafka_handle_error_mode = 'stream'; + +CREATE TABLE + outgoing_webhook_events_cluster ( + `merchant_id` String, + `event_id` String, + `event_type` LowCardinality(String), + `outgoing_webhook_event_type` LowCardinality(String), + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `attempt_id` Nullable(String), + `dispute_id` Nullable(String), + `payment_method_id` Nullable(String), + `mandate_id` Nullable(String), + `content` Nullable(String), + `is_error` Bool, + `error` Nullable(String), + `created_at_timestamp` DateTime64(3), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + INDEX eventIndex event_type TYPE bloom_filter GRANULARITY 1, + INDEX webhookeventIndex outgoing_webhook_event_type TYPE bloom_filter GRANULARITY 1 + ) ENGINE = MergeTree PARTITION BY toStartOfDay(created_at_timestamp) +ORDER BY ( + created_at_timestamp, + merchant_id, + event_id, + event_type, + outgoing_webhook_event_type + ) TTL inserted_at + toIntervalMonth(6); + +CREATE MATERIALIZED VIEW outgoing_webhook_events_mv TO outgoing_webhook_events_cluster ( + `merchant_id` String, + `event_id` Nullable(String), + `event_type` LowCardinality(String), + `outgoing_webhook_event_type` LowCardinality(String), + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `attempt_id` Nullable(String), + `dispute_id` Nullable(String), + `payment_method_id` Nullable(String), + `mandate_id` Nullable(String), + `content` Nullable(String), + `is_error` Bool, + `error` Nullable(String), + `created_at_timestamp` DateTime64(3), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), +) AS +SELECT + merchant_id, + event_id, + event_type, + outgoing_webhook_event_type, + payment_id, + refund_id, + attempt_id, + dispute_id, + payment_method_id, + mandate_id, + content, + is_error, + error, + created_at_timestamp, + now() AS inserted_at +FROM + outgoing_webhook_events_queue +where length(_error) = 0; + +CREATE MATERIALIZED VIEW outgoing_webhook_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 + outgoing_webhook_events_queue +WHERE length(_error) > 0; \ No newline at end of file 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..81b82c5dce15 --- /dev/null +++ b/crates/analytics/src/api_event/core.rs @@ -0,0 +1,181 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + api_event::{ + ApiEventMetricsBucketIdentifier, ApiEventMetricsBucketValue, ApiLogsRequest, + ApiMetricsBucketResponse, + }, + AnalyticsMetadata, ApiEventFiltersResponse, GetApiEventFiltersRequest, + GetApiEventMetricRequest, MetricsResponse, +}; +use common_utils::errors::ReportSwitchExt; +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( + "API Events not implemented for SQLX", + )) + .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 + } + } + .switch()?; + 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( + "API Events not implemented for SQLX", + )) + .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 + } + } + .switch()? + .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..eb9b2d561c53 --- /dev/null +++ b/crates/analytics/src/api_event/events.rs @@ -0,0 +1,107 @@ +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, + pub http_method: Option, + pub url_path: Option, +} 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..27d423505090 --- /dev/null +++ b/crates/analytics/src/clickhouse.rs @@ -0,0 +1,506 @@ +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::{ + health_check::HealthCheck, + 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}, + }, + connector_events::events::ConnectorEventsResult, + outgoing_webhook_event::events::OutgoingWebhookLogsResult, + 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 HealthCheck for ClickhouseClient { + async fn deep_health_check( + &self, + ) -> common_utils::errors::CustomResult<(), QueryExecutionError> { + self.execute_query("SELECT 1") + .await + .map(|_| ()) + .change_context(QueryExecutionError::DatabaseError) + } +} + +#[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, + AnalyticsCollection::ConnectorEvents => TableEngine::BasicTree, + AnalyticsCollection::OutgoingWebhookEvent => 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 {} +impl super::connector_events::events::ConnectorEventLogAnalytics for ClickhouseClient {} +impl super::outgoing_webhook_event::events::OutgoingWebhookLogsFilterAnalytics + 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 ConnectorEventsResult 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 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 OutgoingWebhookLogsResult 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_attempts".to_string()), + Self::Refund => Ok("refunds".to_string()), + Self::SdkEvents => Ok("sdk_events_audit".to_string()), + Self::ApiEvents => Ok("api_events_audit".to_string()), + Self::PaymentIntent => Ok("payment_intents".to_string()), + Self::ConnectorEvents => Ok("connector_events_audit".to_string()), + Self::OutgoingWebhookEvent => Ok("outgoing_webhook_events_audit".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/connector_events.rs b/crates/analytics/src/connector_events.rs new file mode 100644 index 000000000000..c7c31306a2c7 --- /dev/null +++ b/crates/analytics/src/connector_events.rs @@ -0,0 +1,5 @@ +mod core; +pub mod events; +pub trait ConnectorEventAnalytics: events::ConnectorEventLogAnalytics {} + +pub use self::core::connector_events_core; diff --git a/crates/analytics/src/connector_events/core.rs b/crates/analytics/src/connector_events/core.rs new file mode 100644 index 000000000000..15f841af5f82 --- /dev/null +++ b/crates/analytics/src/connector_events/core.rs @@ -0,0 +1,27 @@ +use api_models::analytics::connector_events::ConnectorEventsRequest; +use common_utils::errors::ReportSwitchExt; +use error_stack::{IntoReport, ResultExt}; + +use super::events::{get_connector_events, ConnectorEventsResult}; +use crate::{errors::AnalyticsResult, types::FiltersError, AnalyticsProvider}; + +pub async fn connector_events_core( + pool: &AnalyticsProvider, + req: ConnectorEventsRequest, + merchant_id: String, +) -> AnalyticsResult> { + let data = match pool { + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented( + "Connector Events not implemented for SQLX", + )) + .into_report() + .attach_printable("SQL Analytics is not implemented for Connector Events"), + AnalyticsProvider::Clickhouse(ckh_pool) + | AnalyticsProvider::CombinedSqlx(_, ckh_pool) + | AnalyticsProvider::CombinedCkh(_, ckh_pool) => { + get_connector_events(&merchant_id, req, ckh_pool).await + } + } + .switch()?; + Ok(data) +} diff --git a/crates/analytics/src/connector_events/events.rs b/crates/analytics/src/connector_events/events.rs new file mode 100644 index 000000000000..47044811a8bf --- /dev/null +++ b/crates/analytics/src/connector_events/events.rs @@ -0,0 +1,72 @@ +use api_models::analytics::{connector_events::ConnectorEventsRequest, Granularity}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; +pub trait ConnectorEventLogAnalytics: LoadRow {} + +pub async fn get_connector_events( + merchant_id: &String, + query_param: ConnectorEventsRequest, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + ConnectorEventLogAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::ConnectorEvents); + query_builder.add_select_column("*").switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + query_builder + .add_filter_clause("payment_id", query_param.payment_id) + .switch()?; + + if let Some(refund_id) = query_param.refund_id { + query_builder + .add_filter_clause("refund_id", &refund_id) + .switch()?; + } + + if let Some(dispute_id) = query_param.dispute_id { + query_builder + .add_filter_clause("dispute_id", &dispute_id) + .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 ConnectorEventsResult { + pub merchant_id: String, + pub payment_id: String, + pub connector_name: Option, + pub request_id: Option, + pub flow: String, + pub request: String, + pub response: Option, + pub error: Option, + pub status_code: u16, + pub latency: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + pub method: Option, +} 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/health_check.rs b/crates/analytics/src/health_check.rs new file mode 100644 index 000000000000..f566aecf10bd --- /dev/null +++ b/crates/analytics/src/health_check.rs @@ -0,0 +1,8 @@ +use common_utils::errors::CustomResult; + +use crate::types::QueryExecutionError; + +#[async_trait::async_trait] +pub trait HealthCheck { + async fn deep_health_check(&self) -> CustomResult<(), QueryExecutionError>; +} 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..a4e925519ceb --- /dev/null +++ b/crates/analytics/src/lib.rs @@ -0,0 +1,512 @@ +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 connector_events; +pub mod health_check; +pub mod outgoing_webhook_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/outgoing_webhook_event.rs b/crates/analytics/src/outgoing_webhook_event.rs new file mode 100644 index 000000000000..9919d8bbb0fd --- /dev/null +++ b/crates/analytics/src/outgoing_webhook_event.rs @@ -0,0 +1,6 @@ +mod core; +pub mod events; + +pub trait OutgoingWebhookEventAnalytics: events::OutgoingWebhookLogsFilterAnalytics {} + +pub use self::core::outgoing_webhook_events_core; diff --git a/crates/analytics/src/outgoing_webhook_event/core.rs b/crates/analytics/src/outgoing_webhook_event/core.rs new file mode 100644 index 000000000000..5024cc70ec1c --- /dev/null +++ b/crates/analytics/src/outgoing_webhook_event/core.rs @@ -0,0 +1,27 @@ +use api_models::analytics::outgoing_webhook_event::OutgoingWebhookLogsRequest; +use common_utils::errors::ReportSwitchExt; +use error_stack::{IntoReport, ResultExt}; + +use super::events::{get_outgoing_webhook_event, OutgoingWebhookLogsResult}; +use crate::{errors::AnalyticsResult, types::FiltersError, AnalyticsProvider}; + +pub async fn outgoing_webhook_events_core( + pool: &AnalyticsProvider, + req: OutgoingWebhookLogsRequest, + merchant_id: String, +) -> AnalyticsResult> { + let data = match pool { + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented( + "Outgoing Webhook Events Logs not implemented for SQLX", + )) + .into_report() + .attach_printable("SQL Analytics is not implemented for Outgoing Webhook Events"), + AnalyticsProvider::Clickhouse(ckh_pool) + | AnalyticsProvider::CombinedSqlx(_, ckh_pool) + | AnalyticsProvider::CombinedCkh(_, ckh_pool) => { + get_outgoing_webhook_event(&merchant_id, req, ckh_pool).await + } + } + .switch()?; + Ok(data) +} diff --git a/crates/analytics/src/outgoing_webhook_event/events.rs b/crates/analytics/src/outgoing_webhook_event/events.rs new file mode 100644 index 000000000000..e742387e1eb5 --- /dev/null +++ b/crates/analytics/src/outgoing_webhook_event/events.rs @@ -0,0 +1,90 @@ +use api_models::analytics::{outgoing_webhook_event::OutgoingWebhookLogsRequest, Granularity}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; +pub trait OutgoingWebhookLogsFilterAnalytics: LoadRow {} + +pub async fn get_outgoing_webhook_event( + merchant_id: &String, + query_param: OutgoingWebhookLogsRequest, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + OutgoingWebhookLogsFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::OutgoingWebhookEvent); + query_builder.add_select_column("*").switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_filter_clause("payment_id", query_param.payment_id) + .switch()?; + + if let Some(event_id) = query_param.event_id { + query_builder + .add_filter_clause("event_id", &event_id) + .switch()?; + } + if let Some(refund_id) = query_param.refund_id { + query_builder + .add_filter_clause("refund_id", &refund_id) + .switch()?; + } + if let Some(dispute_id) = query_param.dispute_id { + query_builder + .add_filter_clause("dispute_id", &dispute_id) + .switch()?; + } + if let Some(mandate_id) = query_param.mandate_id { + query_builder + .add_filter_clause("mandate_id", &mandate_id) + .switch()?; + } + if let Some(payment_method_id) = query_param.payment_method_id { + query_builder + .add_filter_clause("payment_method_id", &payment_method_id) + .switch()?; + } + if let Some(attempt_id) = query_param.attempt_id { + query_builder + .add_filter_clause("attempt_id", &attempt_id) + .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 OutgoingWebhookLogsResult { + pub merchant_id: String, + pub event_id: String, + pub event_type: String, + pub outgoing_webhook_event_type: String, + pub payment_id: String, + pub refund_id: Option, + pub attempt_id: Option, + pub dispute_id: Option, + pub payment_method_id: Option, + pub mandate_id: Option, + pub content: Option, + pub is_error: bool, + pub error: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, +} 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..46cc636f4388 --- /dev/null +++ b/crates/analytics/src/sdk_events/core.rs @@ -0,0 +1,206 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + sdk_events::{ + MetricsBucketResponse, SdkEventMetrics, SdkEventMetricsBucketIdentifier, SdkEventsRequest, + }, + AnalyticsMetadata, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, MetricsResponse, + SdkEventFiltersResponse, +}; +use common_utils::errors::ReportSwitchExt; +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( + "SDK Events not implemented for SQLX", + )) + .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 + } + } + .switch() +} + +#[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( + "SDK Events not implemented for SQLX", + )) + .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 61% rename from crates/router/src/analytics/sqlx.rs rename to crates/analytics/src/sqlx.rs index b88a2065f0b0..562a3a1f64d1 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,17 @@ use sqlx::{ Error::ColumnNotFound, FromRow, Pool, Postgres, Row, }; +use storage_impl::config::Database; use time::PrimitiveDateTime; use super::{ - query::{Aggregate, ToSql}, + health_check::HealthCheck, + query::{Aggregate, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, QueryExecutionError, + TableEngine, }, }; -use crate::configs::settings::Database; #[derive(Debug, Clone)] pub struct SqlxClient { @@ -47,19 +46,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 +141,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 {} @@ -177,6 +165,17 @@ impl AnalyticsDataSource for SqlxClient { .change_context(QueryExecutionError::RowExtractionFailure) } } +#[async_trait::async_trait] +impl HealthCheck for SqlxClient { + async fn deep_health_check(&self) -> CustomResult<(), QueryExecutionError> { + sqlx::query("SELECT 1") + .fetch_all(&self.pool) + .await + .map(|_| ()) + .into_report() + .change_context(QueryExecutionError::DatabaseError) + } +} impl<'a> FromRow<'a, PgRow> for super::refunds::metrics::RefundMetricRow { fn from_row(row: &'a PgRow) -> sqlx::Result { @@ -207,7 +206,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 +252,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 +265,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 +343,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 +379,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 +426,25 @@ 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()), + Self::ConnectorEvents => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("ConnectorEvents table is not implemented for Sqlx"))?, + Self::OutgoingWebhookEvent => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("OutgoingWebhookEvents table is not implemented for Sqlx"))?, } } } @@ -367,7 +453,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 +464,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 73% rename from crates/router/src/analytics/types.rs rename to crates/analytics/src/types.rs index fe20e812a9b8..18e9e9f4334b 100644 --- a/crates/router/src/analytics/types.rs +++ b/crates/analytics/src/types.rs @@ -2,25 +2,39 @@ 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; +use crate::errors::AnalyticsError; -#[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, + ConnectorEvents, + OutgoingWebhookEvent, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub enum TableEngine { + CollapsingMergeTree { sign: &'static str }, + BasicTree, } #[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] @@ -50,6 +64,7 @@ where // Analytics Framework pub trait RefundAnalytics {} +pub trait SdkEventAnalytics {} #[async_trait::async_trait] pub trait AnalyticsDataSource @@ -60,6 +75,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 @@ -108,8 +127,8 @@ pub enum FiltersError { #[error("Error running Query")] QueryExecutionFailure, #[allow(dead_code)] - #[error("Not Implemented")] - NotImplemented, + #[error("Not Implemented: {0}")] + NotImplemented(&'static str), } impl ErrorSwitch for QueryBuildingError { @@ -117,3 +136,14 @@ impl ErrorSwitch for QueryBuildingError { FiltersError::QueryBuildingError } } + +impl ErrorSwitch for FiltersError { + fn switch(&self) -> AnalyticsError { + match self { + Self::QueryBuildingError | Self::QueryExecutionFailure => AnalyticsError::UnknownError, + Self::NotImplemented(a) => AnalyticsError::NotImplemented(a), + } + } +} + +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..1e8e0f47eb94 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -8,32 +8,38 @@ readme = "README.md" license.workspace = true [features] -default = ["payouts"] +default = ["payouts", "frm"] business_profile_routing = [] 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 = [] +frm = [] +olap = [] +openapi = ["common_enums/openapi"] +recon = [] [dependencies] actix-web = { version = "4.3.1", optional = true } error-stack = "0.3.1" 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"] } +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" +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"] } +frunk = "0.4.1" +frunk_core = "0.4.1" # First party crates cards = { version = "0.1.0", path = "../cards" } common_enums = { path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } euclid = { version = "0.1.0", path = "../euclid" } -masking = { version = "0.1.0", path = "../masking" } +masking = { version = "0.1.0", path = "../masking", default-features = false, features = ["alloc", "serde"] } router_derive = { version = "0.1.0", path = "../router_derive" } diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 979214a071a9..68ee6cb48b74 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, @@ -30,7 +32,7 @@ pub struct MerchantAccountCreate { #[schema(value_type= Option,example = "NewAge Retailer")] pub merchant_name: Option>, - /// Merchant related details + /// Details about the merchant pub merchant_details: Option, /// The URL to redirect after the completion of the operation @@ -41,7 +43,8 @@ pub struct MerchantAccountCreate { pub webhook_details: Option, /// The routing algorithm to be used for routing payments to desired connectors - #[schema(value_type = Option,example = json!({"type": "single", "data": "stripe"}))] + #[serde(skip)] + #[schema(deprecated)] pub routing_algorithm: Option, /// The routing algorithm to be used for routing payouts to desired connectors @@ -65,8 +68,7 @@ pub struct MerchantAccountCreate { #[schema(default = false, example = true)] pub enable_payment_response_hash: Option, - /// Refers to the hash key used for calculating the signature for webhooks and redirect response - /// If the value is not provided, a default value is used + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -85,7 +87,7 @@ pub struct MerchantAccountCreate { #[schema(example = "locker_abc123")] pub locker_id: Option, - ///Default business details for connector routing + /// Details about the primary business unit of the merchant account #[schema(value_type = Option)] pub primary_business_details: Option>, @@ -93,15 +95,8 @@ pub struct MerchantAccountCreate { #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - ///Will be used to expire client secret after certain amount of time to be supplied in seconds - ///(900) for 15 mins - #[schema(example = 900)] - pub intent_fulfillment_time: Option, - /// The id of the organization to which the merchant belongs to pub organization_id: Option, - - pub payment_link_config: Option, } #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] @@ -122,7 +117,7 @@ pub struct MerchantAccountUpdate { #[schema(example = "NewAge Retailer")] pub merchant_name: Option, - /// Merchant related details + /// Details about the merchant pub merchant_details: Option, /// The URL to redirect after the completion of the operation @@ -133,10 +128,11 @@ pub struct MerchantAccountUpdate { pub webhook_details: Option, /// The routing algorithm to be used for routing payments to desired connectors - #[schema(value_type = Option,example = json!({"type": "single", "data": "stripe"}))] + #[serde(skip)] + #[schema(deprecated)] pub routing_algorithm: Option, - /// The routing algorithm to be used for routing payouts to desired connectors + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] #[serde( @@ -157,7 +153,7 @@ pub struct MerchantAccountUpdate { #[schema(default = false, example = true)] pub enable_payment_response_hash: Option, - /// Refers to the hash key used for payment response + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -176,23 +172,17 @@ pub struct MerchantAccountUpdate { #[schema(example = "locker_abc123")] pub locker_id: Option, - ///Default business details for connector routing + /// Details about the primary business unit of the merchant account pub primary_business_details: Option>, /// The frm routing algorithm to be used for routing payments to desired FRM's #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - ///Will be used to expire client secret after certain amount of time to be supplied in seconds - ///(900) for 15 mins - pub intent_fulfillment_time: Option, - /// The default business profile that must be used for creating merchant accounts and payments /// To unset this field, pass an empty string #[schema(max_length = 64)] pub default_profile: Option, - - pub payment_link_config: Option, } #[derive(Clone, Debug, ToSchema, Serialize)] @@ -213,7 +203,7 @@ pub struct MerchantAccountResponse { #[schema(default = false, example = true)] pub enable_payment_response_hash: bool, - /// Refers to the Parent Merchant ID if the merchant being created is a sub-merchant + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. #[schema(max_length = 255, example = "xkkdf909012sdjki2dkh5sdf")] pub payment_response_hash_key: Option, @@ -221,7 +211,7 @@ pub struct MerchantAccountResponse { #[schema(default = false, example = true)] pub redirect_to_merchant_with_http_post: bool, - /// Merchant related details + /// Details about the merchant #[schema(value_type = Option)] pub merchant_details: Option>, @@ -230,10 +220,11 @@ pub struct MerchantAccountResponse { pub webhook_details: Option, /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' - #[schema(value_type = Option, max_length = 255, example = "custom")] + #[serde(skip)] + #[schema(deprecated)] pub routing_algorithm: Option, - /// The routing algorithm to be used for routing payouts to desired connectors + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] #[serde( @@ -261,7 +252,8 @@ pub struct MerchantAccountResponse { /// An identifier for the vault used to store payment method information. #[schema(example = "locker_abc123")] pub locker_id: Option, - ///Default business details for connector routing + + /// Details about the primary business unit of the merchant account #[schema(value_type = Vec)] pub primary_business_details: Vec, @@ -283,11 +275,9 @@ pub struct MerchantAccountResponse { #[schema(max_length = 64)] pub default_profile: Option, - /// A enum value to indicate the status of recon service. By default it is not_requested. + /// Used to indicate the status of the recon module for a merchant account #[schema(value_type = ReconStatus, example = "not_requested")] pub recon_status: enums::ReconStatus, - - pub payment_link_config: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] @@ -357,7 +347,7 @@ pub mod payout_routing_algorithm { where A: de::MapAccess<'de>, { - let mut output = serde_json::Value::Object(Map::new()); + let mut output = Map::new(); let mut routing_data: String = "".to_string(); let mut routing_type: String = "".to_string(); @@ -365,14 +355,20 @@ pub mod payout_routing_algorithm { match key { "type" => { routing_type = map.next_value()?; - output["type"] = serde_json::Value::String(routing_type.to_owned()); + output.insert( + "type".to_string(), + serde_json::Value::String(routing_type.to_owned()), + ); } "data" => { routing_data = map.next_value()?; - output["data"] = serde_json::Value::String(routing_data.to_owned()); + output.insert( + "data".to_string(), + serde_json::Value::String(routing_data.to_owned()), + ); } f => { - output[f] = map.next_value()?; + output.insert(f.to_string(), map.next_value()?); } } } @@ -390,7 +386,7 @@ pub mod payout_routing_algorithm { } u => Err(de::Error::custom(format!("Unknown routing algorithm {u}"))), }?; - Ok(output) + Ok(serde_json::Value::Object(output)) } } @@ -452,21 +448,6 @@ pub struct PrimaryBusinessDetails { pub business: String, } -#[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] -#[serde(deny_unknown_fields)] -pub struct PaymentLinkConfig { - pub merchant_logo: Option, - pub color_scheme: Option, -} - -#[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] -#[serde(deny_unknown_fields)] - -pub struct PaymentLinkColorSchema { - pub background_primary_color: Option, - pub sdk_theme: Option, -} - #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] #[serde(deny_unknown_fields)] pub struct WebhookDetails { @@ -530,23 +511,18 @@ pub struct MerchantConnectorCreate { /// Name of the Connector #[schema(value_type = Connector, example = "stripe")] pub connector_name: api_enums::Connector, - /// Connector label for a connector, this can serve as a field to identify the connector as per business details + /// This is an unique label you can generate and pass in order to identify this connector account on your Hyperswitch dashboard and reports. Eg: if your profile label is `default`, connector label can be `stripe_default` #[schema(example = "stripe_US_travel")] pub connector_label: Option, - /// Unique ID of the connector - #[schema(example = "mca_5apGeP94tMts6rg3U3kR")] - pub merchant_connector_id: Option, - /// Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object. - #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] + /// Identifier for the business profile, if not provided default will be chosen from merchant account + pub profile_id: Option, + + /// An object containing the required details/credentials for a Connector account. + #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] pub connector_account_details: Option, - /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. - #[schema(default = false, example = false)] - pub test_mode: Option, - /// A boolean value to indicate if the connector is disabled. By default, its value is false. - #[schema(default = false, example = false)] - pub disabled: Option, - /// Refers to the Parent Merchant ID if the merchant being created is a sub-merchant + + /// An object containing the details about the payment methods that need to be enabled under this merchant connector account #[schema(example = json!([ { "payment_method": "wallet", @@ -577,33 +553,80 @@ pub struct MerchantConnectorCreate { } ]))] pub payment_methods_enabled: Option>, + + /// Webhook details of this merchant connector + #[schema(example = json!({ + "connector_webhook_details": { + "merchant_secret": "1234567890987654321" + } + }))] + pub connector_webhook_details: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, - /// contains the frm configs for the merchant connector + + /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. + #[schema(default = false, example = false)] + pub test_mode: Option, + + /// A boolean value to indicate if the connector is disabled. By default, its value is false. + #[schema(default = false, example = false)] + pub disabled: Option, + + /// Contains the frm configs for the merchant connector #[schema(example = json!(common_utils::consts::FRM_CONFIGS_EG))] pub frm_configs: Option>, + /// The business country to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead #[schema(value_type = Option, example = "US")] pub business_country: Option, + /// The business label to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead pub business_label: Option, - /// Business Sub label of the merchant + /// The business sublabel to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead #[schema(example = "chase")] pub business_sub_label: Option, - /// Webhook details of this merchant connector - #[schema(example = json!({ - "connector_webhook_details": { - "merchant_secret": "1234567890987654321" - } - }))] - pub connector_webhook_details: Option, - /// Identifier for the business profile, if not provided default will be chosen from merchant account - pub profile_id: Option, + /// Unique ID of the connector + #[schema(example = "mca_5apGeP94tMts6rg3U3kR")] + pub merchant_connector_id: Option, pub pm_auth_config: Option, + + #[schema(value_type = Option, 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)] @@ -623,26 +646,26 @@ pub struct MerchantConnectorResponse { #[schema(value_type = ConnectorType, example = "payment_processor")] pub connector_type: api_enums::ConnectorType, /// Name of the Connector - #[schema(example = "stripe")] + #[schema(value_type = Connector, example = "stripe")] pub connector_name: String, - /// Connector label for a connector, this can serve as a field to identify the connector as per business details + /// A unique label to identify the connector account created under a business profile #[schema(example = "stripe_US_travel")] pub connector_label: Option, - /// Unique ID of the connector + /// Unique ID of the merchant connector account #[schema(example = "mca_5apGeP94tMts6rg3U3kR")] pub merchant_connector_id: String, - /// Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object. - #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] + + /// Identifier for the business profile, if not provided default will be chosen from merchant account + #[schema(max_length = 64)] + pub profile_id: Option, + + /// An object containing the required details/credentials for a Connector account. + #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] pub connector_account_details: pii::SecretSerdeValue, - /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. - #[schema(default = false, example = false)] - pub test_mode: Option, - /// A boolean value to indicate if the connector is disabled. By default, its value is false. - #[schema(default = false, example = false)] - pub disabled: Option, - /// Refers to the Parent Merchant ID if the merchant being created is a sub-merchant + + /// An object containing the details about the payment methods that need to be enabled under this merchant connector account #[schema(example = json!([ { "payment_method": "wallet", @@ -673,42 +696,50 @@ pub struct MerchantConnectorResponse { } ]))] pub payment_methods_enabled: Option>, + + /// Webhook details of this merchant connector + #[schema(example = json!({ + "connector_webhook_details": { + "merchant_secret": "1234567890987654321" + } + }))] + pub connector_webhook_details: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, - /// Business Country of the connector + /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. + #[schema(default = false, example = false)] + pub test_mode: Option, + + /// A boolean value to indicate if the connector is disabled. By default, its value is false. + #[schema(default = false, example = false)] + pub disabled: Option, + + /// Contains the frm configs for the merchant connector + #[schema(example = json!(common_utils::consts::FRM_CONFIGS_EG))] + pub frm_configs: Option>, + + /// The business country to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead #[schema(value_type = Option, example = "US")] pub business_country: Option, - ///Business Type of the merchant + ///The business label to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead #[schema(example = "travel")] pub business_label: Option, - /// Business Sub label of the merchant + /// The business sublabel to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead #[schema(example = "chase")] pub business_sub_label: Option, - /// contains the frm configs for the merchant connector - #[schema(example = json!(common_utils::consts::FRM_CONFIGS_EG))] - pub frm_configs: Option>, - - /// Webhook details of this merchant connector - #[schema(example = json!({ - "connector_webhook_details": { - "merchant_secret": "1234567890987654321" - } - }))] - pub connector_webhook_details: Option, - - /// The business profile this connector must be created in - /// default value from merchant account is taken if not passed - #[schema(max_length = 64)] - pub profile_id: Option, /// identifier for the verified domains of a particular connector account 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." @@ -719,22 +750,15 @@ pub struct MerchantConnectorUpdate { #[schema(value_type = ConnectorType, example = "payment_processor")] pub connector_type: api_enums::ConnectorType, - /// Connector label for a connector, this can serve as a field to identify the connector as per business details + /// This is an unique label you can generate and pass in order to identify this connector account on your Hyperswitch dashboard and reports. Eg: if your profile label is `default`, connector label can be `stripe_default` + #[schema(example = "stripe_US_travel")] pub connector_label: Option, - /// Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object. - #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] + /// An object containing the required details/credentials for a Connector account. + #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] pub connector_account_details: Option, - /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. - #[schema(default = false, example = false)] - pub test_mode: Option, - - /// A boolean value to indicate if the connector is disabled. By default, its value is false. - #[schema(default = false, example = false)] - pub disabled: Option, - - /// Refers to the Parent Merchant ID if the merchant being created is a sub-merchant + /// An object containing the details about the payment methods that need to be enabled under this merchant connector account #[schema(example = json!([ { "payment_method": "wallet", @@ -766,14 +790,6 @@ pub struct MerchantConnectorUpdate { ]))] pub payment_methods_enabled: Option>, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. - #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] - pub metadata: Option, - - /// contains the frm configs for the merchant connector - #[schema(example = json!(common_utils::consts::FRM_CONFIGS_EG))] - pub frm_configs: Option>, - /// Webhook details of this merchant connector #[schema(example = json!({ "connector_webhook_details": { @@ -782,7 +798,26 @@ pub struct MerchantConnectorUpdate { }))] pub connector_webhook_details: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] + pub metadata: Option, + + /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. + #[schema(default = false, example = false)] + pub test_mode: Option, + + /// A boolean value to indicate if the connector is disabled. By default, its value is false. + #[schema(default = false, example = false)] + pub disabled: Option, + + /// Contains the frm configs for the merchant connector + #[schema(example = json!(common_utils::consts::FRM_CONFIGS_EG))] + pub frm_configs: 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 @@ -833,7 +868,7 @@ pub struct PaymentMethodsEnabled { pub payment_method: common_enums::PaymentMethod, /// Subtype of payment method - #[schema(value_type = Option>,example = json!(["credit"]))] + #[schema(value_type = Option>,example = json!(["credit"]))] pub payment_method_types: Option>, } @@ -859,6 +894,7 @@ pub enum AcceptedCurrencies { content = "list", rename_all = "snake_case" )] +/// Object to filter the customer countries for which the payment method is displayed pub enum AcceptedCountries { #[schema(value_type = Vec)] EnableOnly(Vec), @@ -944,12 +980,11 @@ pub enum PayoutStraightThroughAlgorithm { #[derive(Clone, Debug, Deserialize, ToSchema, Default, Serialize)] #[serde(deny_unknown_fields)] pub struct BusinessProfileCreate { - /// A short name to identify the business profile + /// The name of business profile #[schema(max_length = 64)] pub profile_name: Option, - /// The URL to redirect after the completion of the operation, This will be applied to all the - /// connector accounts under this profile + /// The URL to redirect after the completion of the operation #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] pub return_url: Option, @@ -957,8 +992,7 @@ pub struct BusinessProfileCreate { #[schema(default = true, example = true)] pub enable_payment_response_hash: Option, - /// Refers to the hash key used for calculating the signature for webhooks and redirect response - /// If the value is not provided, a default value is used + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -985,7 +1019,7 @@ pub struct BusinessProfileCreate { #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - /// The routing algorithm to be used for routing payouts to desired connectors + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] #[serde( @@ -996,6 +1030,13 @@ pub struct BusinessProfileCreate { /// Verified applepay domains for a particular profile pub applepay_verified_domains: Option>, + + /// Client Secret Default expiry for all payments created under this business profile + #[schema(example = 900)] + pub session_expiry: Option, + + /// Default Payment Link config for all payment links created under this business profile + pub payment_link_config: Option, } #[derive(Clone, Debug, ToSchema, Serialize)] @@ -1004,16 +1045,15 @@ pub struct BusinessProfileResponse { #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44")] pub merchant_id: String, - /// The unique identifier for Business Profile + /// The default business profile that must be used for creating merchant accounts and payments #[schema(max_length = 64, example = "pro_abcdefghijklmnopqrstuvwxyz")] pub profile_id: String, - /// A short name to identify the business profile + /// Name of the business profile #[schema(max_length = 64)] pub profile_name: String, - /// The URL to redirect after the completion of the operation, This will be applied to all the - /// connector accounts under this profile + /// The URL to redirect after the completion of the operation #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] pub return_url: Option, @@ -1021,8 +1061,7 @@ pub struct BusinessProfileResponse { #[schema(default = true, example = true)] pub enable_payment_response_hash: bool, - /// Refers to the hash key used for calculating the signature for webhooks and redirect response - /// If the value is not provided, a default value is used + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -1030,6 +1069,7 @@ pub struct BusinessProfileResponse { pub redirect_to_merchant_with_http_post: bool, /// Webhook related details + #[schema(value_type = Option)] pub webhook_details: Option, /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. @@ -1045,11 +1085,11 @@ pub struct BusinessProfileResponse { #[schema(example = 900)] pub intent_fulfillment_time: Option, - /// The frm routing algorithm to be used for routing payments to desired FRM's + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - /// The routing algorithm to be used for routing payouts to desired connectors + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] #[serde( @@ -1060,17 +1100,23 @@ pub struct BusinessProfileResponse { /// Verified applepay domains for a particular profile pub applepay_verified_domains: Option>, + + /// Client Secret Default expiry for all payments created under this business profile + #[schema(example = 900)] + pub session_expiry: Option, + + /// Default Payment Link config for all payment links created under this business profile + pub payment_link_config: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] #[serde(deny_unknown_fields)] pub struct BusinessProfileUpdate { - /// A short name to identify the business profile + /// The name of business profile #[schema(max_length = 64)] pub profile_name: Option, - /// The URL to redirect after the completion of the operation, This will be applied to all the - /// connector accounts under this profile + /// The URL to redirect after the completion of the operation #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] pub return_url: Option, @@ -1078,8 +1124,7 @@ pub struct BusinessProfileUpdate { #[schema(default = true, example = true)] pub enable_payment_response_hash: Option, - /// Refers to the hash key used for calculating the signature for webhooks and redirect response - /// If the value is not provided, a default value is used + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -1106,7 +1151,7 @@ pub struct BusinessProfileUpdate { #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - /// The routing algorithm to be used for routing payouts to desired connectors + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] #[serde( @@ -1117,4 +1162,46 @@ pub struct BusinessProfileUpdate { /// Verified applepay domains for a particular profile pub applepay_verified_domains: Option>, + + /// Client Secret Default expiry for all payments created under this business profile + #[schema(example = 900)] + pub session_expiry: Option, + + /// Default Payment Link config for all payment links created under this business profile + pub payment_link_config: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] +pub struct BusinessPaymentLinkConfig { + pub domain_name: Option, + #[serde(flatten)] + pub config: PaymentLinkConfigRequest, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] +pub struct PaymentLinkConfigRequest { + /// custom theme for the payment link + #[schema(value_type = Option, max_length = 255, example = "#4E6ADD")] + pub theme: Option, + /// merchant display logo + #[schema(value_type = Option, max_length = 255, example = "https://i.pinimg.com/736x/4d/83/5c/4d835ca8aafbbb15f84d07d926fda473.jpg")] + pub logo: Option, + /// Custom merchant name for payment link + #[schema(value_type = Option, max_length = 255, example = "hyperswitch")] + pub seller_name: Option, + /// Custom layout for sdk + #[schema(value_type = Option, max_length = 255, example = "accordion")] + pub sdk_layout: Option, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, ToSchema)] +pub struct PaymentLinkConfig { + /// custom theme for the payment link + pub theme: String, + /// merchant display logo + pub logo: String, + /// Custom merchant name for payment link + pub seller_name: String, + /// Custom layout for sdk + pub sdk_layout: String, } diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 0358b6b313cf..c6ca215f9f7c 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -1,15 +1,22 @@ 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 connector_events; +pub mod outgoing_webhook_event; pub mod payments; pub mod refunds; +pub mod sdk_events; #[derive(Debug, serde::Serialize)] pub struct NameDescription { @@ -25,23 +32,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 +53,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 +63,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 +121,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 +148,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 +161,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/connector_events.rs b/crates/api_models/src/analytics/connector_events.rs new file mode 100644 index 000000000000..7d7e4ae2a8bc --- /dev/null +++ b/crates/api_models/src/analytics/connector_events.rs @@ -0,0 +1,6 @@ +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct ConnectorEventsRequest { + pub payment_id: String, + pub refund_id: Option, + pub dispute_id: Option, +} diff --git a/crates/api_models/src/analytics/outgoing_webhook_event.rs b/crates/api_models/src/analytics/outgoing_webhook_event.rs new file mode 100644 index 000000000000..b6f0aca056fd --- /dev/null +++ b/crates/api_models/src/analytics/outgoing_webhook_event.rs @@ -0,0 +1,10 @@ +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct OutgoingWebhookLogsRequest { + pub payment_id: String, + pub event_id: Option, + pub refund_id: Option, + pub dispute_id: Option, + pub mandate_id: Option, + pub payment_method_id: Option, + pub attempt_id: Option, +} 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/blocklist.rs b/crates/api_models/src/blocklist.rs new file mode 100644 index 000000000000..888b9106cccc --- /dev/null +++ b/crates/api_models/src/blocklist.rs @@ -0,0 +1,44 @@ +use common_enums::enums; +use common_utils::events::ApiEventMetric; +use utoipa::ToSchema; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(rename_all = "snake_case", tag = "type", content = "data")] +pub enum BlocklistRequest { + CardBin(String), + Fingerprint(String), + ExtendedCardBin(String), +} + +pub type AddToBlocklistRequest = BlocklistRequest; +pub type DeleteFromBlocklistRequest = BlocklistRequest; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct BlocklistResponse { + pub fingerprint_id: String, + #[schema(value_type = BlocklistDataKind)] + pub data_kind: enums::BlocklistDataKind, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: time::PrimitiveDateTime, +} + +pub type AddToBlocklistResponse = BlocklistResponse; +pub type DeleteFromBlocklistResponse = BlocklistResponse; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct ListBlocklistQuery { + #[schema(value_type = BlocklistDataKind)] + pub data_kind: enums::BlocklistDataKind, + #[serde(default = "default_list_limit")] + pub limit: u16, + #[serde(default)] + pub offset: u16, +} + +fn default_list_limit() -> u16 { + 10 +} + +impl ApiEventMetric for BlocklistRequest {} +impl ApiEventMetric for BlocklistResponse {} +impl ApiEventMetric for ListBlocklistQuery {} 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/connector_onboarding.rs b/crates/api_models/src/connector_onboarding.rs new file mode 100644 index 000000000000..7e8288d9747f --- /dev/null +++ b/crates/api_models/src/connector_onboarding.rs @@ -0,0 +1,60 @@ +use super::{admin, enums}; + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct ActionUrlRequest { + pub connector: enums::Connector, + pub connector_id: String, + pub return_url: String, +} + +#[derive(serde::Serialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ActionUrlResponse { + PayPal(PayPalActionUrlResponse), +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct OnboardingSyncRequest { + pub profile_id: String, + pub connector_id: String, + pub connector: enums::Connector, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct PayPalActionUrlResponse { + pub action_url: String, +} + +#[derive(serde::Serialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum OnboardingStatus { + PayPal(PayPalOnboardingStatus), +} + +#[derive(serde::Serialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum PayPalOnboardingStatus { + AccountNotFound, + PaymentsNotReceivable, + PpcpCustomDenied, + MorePermissionsNeeded, + EmailNotVerified, + Success(PayPalOnboardingDone), + ConnectorIntegrated(admin::MerchantConnectorResponse), +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct PayPalOnboardingDone { + pub payer_id: String, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct PayPalIntegrationDone { + pub connector_id: String, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct ResetTrackingIdRequest { + pub connector_id: String, + pub connector: enums::Connector, +} 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 c4e4aa90c4b8..cc2052d18a99 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + pub use common_enums::*; use utoipa::ToSchema; @@ -11,11 +13,9 @@ use utoipa::ToSchema; serde::Serialize, strum::Display, strum::EnumString, - ToSchema, )] /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' -#[schema(example = "custom")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoutingAlgorithm { @@ -25,6 +25,7 @@ pub enum RoutingAlgorithm { Custom, } +/// A connector is an integration to fulfill payments #[derive( Clone, Copy, @@ -107,6 +108,7 @@ pub enum Connector { Payme, Paypal, Payu, + Placetopay, Powertranz, Prophetpay, Rapyd, @@ -124,6 +126,7 @@ pub enum Connector { Zen, Signifyd, Plaid, + Riskified, } impl Connector { @@ -147,6 +150,7 @@ impl Connector { } } +#[cfg(feature = "payouts")] #[derive( Clone, Copy, @@ -158,94 +162,26 @@ impl Connector { serde::Deserialize, strum::Display, strum::EnumString, - strum::EnumIter, - strum::EnumVariantNames, + ToSchema, )] #[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, +pub enum PayoutConnectors { 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, } #[cfg(feature = "payouts")] +impl From for RoutableConnectors { + fn from(value: PayoutConnectors) -> Self { + match value { + PayoutConnectors::Adyen => Self::Adyen, + PayoutConnectors::Wise => Self::Wise, + } + } +} + +#[cfg(feature = "frm")] #[derive( Clone, Copy, @@ -261,17 +197,18 @@ pub enum RoutableConnectors { )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] -pub enum PayoutConnectors { - Adyen, - Wise, +pub enum FrmConnectors { + /// Signifyd Risk Manager. Official docs: https://docs.signifyd.com/ + Signifyd, + Riskified, } -#[cfg(feature = "payouts")] -impl From for RoutableConnectors { - fn from(value: PayoutConnectors) -> Self { +#[cfg(feature = "frm")] +impl From for RoutableConnectors { + fn from(value: FrmConnectors) -> Self { match value { - PayoutConnectors::Adyen => Self::Adyen, - PayoutConnectors::Wise => Self::Wise, + FrmConnectors::Signifyd => Self::Signifyd, + FrmConnectors::Riskified => Self::Riskified, } } } @@ -531,8 +468,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, @@ -568,3 +505,26 @@ pub enum LockerChoice { Basilisk, Tartarus, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + frunk::LabelledGeneric, + ToSchema, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PmAuthConnectors { + Plaid, +} + +pub fn convert_pm_auth_connector(connector_name: &str) -> Option { + PmAuthConnectors::from_str(connector_name).ok() +} 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 0ce7638b5ed1..ae0525ac6098 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -1,21 +1,38 @@ +pub mod connector_onboarding; pub mod customer; +pub mod dispute; pub mod gsm; mod locker_migration; pub mod payment; #[cfg(feature = "payouts")] pub mod payouts; +#[cfg(feature = "recon")] +pub mod recon; pub mod refund; pub mod routing; pub mod user; +pub mod user_role; use common_utils::{ events::{ApiEventMetric, ApiEventsType}, impl_misc_api_event_type, }; +#[allow(unused_imports)] use crate::{ - admin::*, api_keys::*, cards_info::*, disputes::*, files::*, mandates::*, payment_methods::*, - payments::*, verifications::*, + admin::*, + analytics::{ + api_event::*, connector_events::ConnectorEventsRequest, + outgoing_webhook_event::OutgoingWebhookLogsRequest, sdk_events::*, *, + }, + api_keys::*, + cards_info::*, + disputes::*, + files::*, + mandates::*, + payment_methods::*, + payments::*, + verifications::*, }; impl ApiEventMetric for TimeRange {} @@ -23,19 +40,17 @@ impl ApiEventMetric for TimeRange {} impl_misc_api_event_type!( PaymentMethodId, PaymentsSessionResponse, - PaymentMethodListResponse, PaymentMethodCreate, PaymentLinkInitiateRequest, RetrievePaymentLinkResponse, MandateListConstraints, CreateFileResponse, - DisputeResponse, - SubmitEvidenceRequest, MerchantConnectorResponse, MerchantConnectorId, MandateResponse, MandateRevokedResponse, RetrievePaymentLinkRequest, + PaymentLinkListConstraints, MandateId, DisputeListConstraints, RetrieveApiKeyResponse, @@ -62,7 +77,25 @@ 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, + ConnectorEventsRequest, + OutgoingWebhookLogsRequest ); #[cfg(feature = "stripe")] @@ -75,3 +108,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/connector_onboarding.rs b/crates/api_models/src/events/connector_onboarding.rs new file mode 100644 index 000000000000..0da89f61da7e --- /dev/null +++ b/crates/api_models/src/events/connector_onboarding.rs @@ -0,0 +1,14 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::connector_onboarding::{ + ActionUrlRequest, ActionUrlResponse, OnboardingStatus, OnboardingSyncRequest, + ResetTrackingIdRequest, +}; + +common_utils::impl_misc_api_event_type!( + ActionUrlRequest, + ActionUrlResponse, + OnboardingSyncRequest, + OnboardingStatus, + ResetTrackingIdRequest +); diff --git a/crates/api_models/src/events/dispute.rs b/crates/api_models/src/events/dispute.rs new file mode 100644 index 000000000000..101dba3ca028 --- /dev/null +++ b/crates/api_models/src/events/dispute.rs @@ -0,0 +1,25 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use super::{DisputeResponse, DisputeResponsePaymentsRetrieve, SubmitEvidenceRequest}; + +impl ApiEventMetric for SubmitEvidenceRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Dispute { + dispute_id: self.dispute_id.clone(), + }) + } +} +impl ApiEventMetric for DisputeResponsePaymentsRetrieve { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Dispute { + dispute_id: self.dispute_id.clone(), + }) + } +} +impl ApiEventMetric for DisputeResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Dispute { + dispute_id: self.dispute_id.clone(), + }) + } +} diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 2f3336fc2777..32d3dc30bd8d 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -3,13 +3,14 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::{ payment_methods::{ CustomerPaymentMethodsListResponse, PaymentMethodDeleteResponse, PaymentMethodListRequest, - PaymentMethodResponse, PaymentMethodUpdate, + PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate, }, payments::{ PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, - PaymentsCaptureRequest, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, - PaymentsRetrieveRequest, PaymentsStartRequest, RedirectionResponse, + PaymentsCaptureRequest, PaymentsIncrementalAuthorizationRequest, PaymentsRejectRequest, + PaymentsRequest, PaymentsResponse, PaymentsRetrieveRequest, PaymentsStartRequest, + RedirectionResponse, }, }; impl ApiEventMetric for PaymentsRetrieveRequest { @@ -118,6 +119,8 @@ impl ApiEventMetric for PaymentMethodListRequest { } } +impl ApiEventMetric for PaymentMethodListResponse {} + impl ApiEventMetric for PaymentListFilterConstraints { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::ResourceListAPI) @@ -149,3 +152,11 @@ impl ApiEventMetric for PaymentListResponseV2 { } impl ApiEventMetric for RedirectionResponse {} + +impl ApiEventMetric for PaymentsIncrementalAuthorizationRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} diff --git a/crates/api_models/src/events/recon.rs b/crates/api_models/src/events/recon.rs new file mode 100644 index 000000000000..aed648f4c869 --- /dev/null +++ b/crates/api_models/src/events/recon.rs @@ -0,0 +1,21 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::recon::{ReconStatusResponse, ReconTokenResponse, ReconUpdateMerchantRequest}; + +impl ApiEventMetric for ReconUpdateMerchantRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Recon) + } +} + +impl ApiEventMetric for ReconTokenResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Recon) + } +} + +impl ApiEventMetric for ReconStatusResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Recon) + } +} diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 2a896cc38776..04aabc071aea 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -1,8 +1,23 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; +#[cfg(feature = "recon")] +use masking::PeekInterface; -use crate::user::{ConnectAccountRequest, ConnectAccountResponse}; +#[cfg(feature = "dummy_connector")] +use crate::user::sample_data::SampleDataRequest; +#[cfg(feature = "recon")] +use crate::user::VerifyTokenResponse; +use crate::user::{ + dashboard_metadata::{ + GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, + }, + AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, + DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, + InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignInResponse, + SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, + UpdateUserAccountDetailsRequest, UserMerchantCreate, VerifyEmailRequest, +}; -impl ApiEventMetric for ConnectAccountResponse { +impl ApiEventMetric for DashboardEntryResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::User { merchant_id: self.merchant_id.clone(), @@ -11,4 +26,39 @@ impl ApiEventMetric for ConnectAccountResponse { } } -impl ApiEventMetric for ConnectAccountRequest {} +#[cfg(feature = "recon")] +impl ApiEventMetric for VerifyTokenResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::User { + merchant_id: self.merchant_id.clone(), + user_id: self.user_email.peek().to_string(), + }) + } +} + +common_utils::impl_misc_api_event_type!( + SignUpRequest, + SignUpWithMerchantIdRequest, + ChangePasswordRequest, + GetMultipleMetaDataPayload, + GetMetaDataResponse, + GetMetaDataRequest, + SetMetaDataRequest, + SwitchMerchantIdRequest, + CreateInternalUserRequest, + UserMerchantCreate, + GetUsersResponse, + AuthorizeResponse, + ConnectAccountRequest, + ForgotPasswordRequest, + ResetPasswordRequest, + InviteUserRequest, + InviteUserResponse, + VerifyEmailRequest, + SendVerifyEmailRequest, + SignInResponse, + UpdateUserAccountDetailsRequest +); + +#[cfg(feature = "dummy_connector")] +common_utils::impl_misc_api_event_type!(SampleDataRequest); 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..3ec30d6bd975 --- /dev/null +++ b/crates/api_models/src/events/user_role.rs @@ -0,0 +1,16 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::user_role::{ + AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest, GetRoleRequest, + ListRolesResponse, RoleInfoResponse, UpdateUserRoleRequest, +}; + +common_utils::impl_misc_api_event_type!( + ListRolesResponse, + RoleInfoResponse, + GetRoleRequest, + AuthorizationInfoResponse, + UpdateUserRoleRequest, + AcceptInvitationRequest, + DeleteUserRoleRequest +); diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs index 254981b1f8f7..30e49062439f 100644 --- a/crates/api_models/src/gsm.rs +++ b/crates/api_models/src/gsm.rs @@ -4,23 +4,41 @@ use crate::enums::Connector; #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmCreateRequest { + /// The connector through which payment has gone through pub connector: Connector, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, + /// message received from the connector pub message: String, + /// status provided by the router pub status: String, + /// optional error provided by the router pub router_error: Option, + /// decision to be taken for auto retries flow pub decision: GsmDecision, + /// indicates if step_up retry is possible pub step_up_possible: bool, + /// error code unified across the connectors + pub unified_code: Option, + /// error message unified across the connectors + pub unified_message: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmRetrieveRequest { + /// The connector through which payment has gone through pub connector: Connector, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, + /// message received from the connector pub message: String, } @@ -48,44 +66,79 @@ pub enum GsmDecision { #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmUpdateRequest { + /// The connector through which payment has gone through pub connector: String, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, + /// message received from the connector pub message: String, + /// status provided by the router pub status: Option, + /// optional error provided by the router pub router_error: Option, + /// decision to be taken for auto retries flow pub decision: Option, + /// indicates if step_up retry is possible pub step_up_possible: Option, + /// error code unified across the connectors + pub unified_code: Option, + /// error message unified across the connectors + pub unified_message: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmDeleteRequest { + /// The connector through which payment has gone through pub connector: String, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, + /// message received from the connector pub message: String, } #[derive(Debug, serde::Serialize, ToSchema)] pub struct GsmDeleteResponse { pub gsm_rule_delete: bool, + /// The connector through which payment has gone through pub connector: String, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, } #[derive(serde::Serialize, Debug, ToSchema)] pub struct GsmResponse { + /// The connector through which payment has gone through pub connector: String, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, + /// message received from the connector pub message: String, + /// status provided by the router pub status: String, + /// optional error provided by the router pub router_error: Option, + /// decision to be taken for auto retries flow pub decision: String, + /// indicates if step_up retry is possible pub step_up_possible: bool, + /// error code unified across the connectors + pub unified_code: Option, + /// error message unified across the connectors + pub unified_message: Option, } diff --git a/crates/api_models/src/health_check.rs b/crates/api_models/src/health_check.rs new file mode 100644 index 000000000000..e4611f43bcf8 --- /dev/null +++ b/crates/api_models/src/health_check.rs @@ -0,0 +1,18 @@ +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RouterHealthCheckResponse { + pub database: bool, + pub redis: bool, + pub locker: bool, + #[cfg(feature = "olap")] + pub analytics: bool, + pub outgoing_request: bool, +} + +impl common_utils::events::ApiEventMetric for RouterHealthCheckResponse {} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SchedulerHealthCheckResponse { + pub database: bool, + pub redis: bool, +} + +impl common_utils::events::ApiEventMetric for SchedulerHealthCheckResponse {} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 40faa6b3e81d..1ea79ff6fe8f 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -3,7 +3,11 @@ pub mod admin; pub mod analytics; pub mod api_keys; pub mod bank_accounts; +pub mod blocklist; pub mod cards_info; +pub mod conditional_configs; +pub mod connector_onboarding; +pub mod currency; pub mod customers; pub mod disputes; pub mod enums; @@ -13,6 +17,7 @@ pub mod errors; pub mod events; pub mod files; pub mod gsm; +pub mod health_check; pub mod locker_migration; pub mod mandates; pub mod organization; @@ -20,8 +25,14 @@ pub mod payment_methods; pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; +pub mod pm_auth; +#[cfg(feature = "recon")] +pub mod recon; 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/mandates.rs b/crates/api_models/src/mandates.rs index 035f7adec9f7..b29f4e0d0c34 100644 --- a/crates/api_models/src/mandates.rs +++ b/crates/api_models/src/mandates.rs @@ -17,6 +17,12 @@ pub struct MandateRevokedResponse { /// The status for mandates #[schema(value_type = MandateStatus)] pub status: api_enums::MandateStatus, + /// If there was an error while calling the connectors the code is received here + #[schema(example = "E0001")] + pub error_code: Option, + /// If there was an error while calling the connector the error message is received here + #[schema(example = "Failed while verifying the card")] + pub error_message: Option, } #[derive(Default, Debug, Deserialize, Serialize, ToSchema, Clone)] @@ -30,6 +36,8 @@ pub struct MandateResponse { pub payment_method_id: String, /// The payment method pub payment_method: String, + /// The payment method type + pub payment_method_type: Option, /// The card details for mandate pub card: Option, /// Details about the customer’s acceptance @@ -60,6 +68,15 @@ pub struct MandateCardDetails { #[schema(value_type = Option)] /// A unique identifier alias to identify a particular card pub card_fingerprint: Option>, + /// The first 6 digits of card + pub card_isin: Option, + /// The bank that issued the card + pub card_issuer: Option, + /// The network that facilitates payment card transactions + #[schema(value_type = Option)] + pub card_network: Option, + /// The type of the payment card + pub card_type: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 755acbf7f425..173272e25d80 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2,26 +2,26 @@ use std::collections::HashMap; use cards::CardNumber; use common_utils::{ - consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, crypto::OptionalEncryptableName, pii, - types::Percentage, + consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, + crypto::OptionalEncryptableName, + pii, + types::{Percentage, Surcharge}, }; use serde::de; -use utoipa::ToSchema; +use utoipa::{schema, ToSchema}; #[cfg(feature = "payouts")] use crate::payouts; use crate::{ - admin, - customers::CustomerId, - enums as api_enums, - payments::{self, BankCodeResponse, RequestSurchargeDetails}, + admin, enums as api_enums, + payments::{self, BankCodeResponse}, }; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct PaymentMethodCreate { /// The type of payment method use for the payment. - #[schema(value_type = PaymentMethodType,example = "card")] + #[schema(value_type = PaymentMethod,example = "card")] pub payment_method: api_enums::PaymentMethod, /// This is a sub-category of payment method. @@ -55,6 +55,11 @@ pub struct PaymentMethodCreate { /// The card network #[schema(example = "Visa")] pub card_network: Option, + + /// Payment method details from locker + #[cfg(feature = "payouts")] + #[schema(value_type = Option)] + pub bank_transfer: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -72,6 +77,11 @@ pub struct PaymentMethodUpdate { #[schema(value_type = Option,example = "Visa")] pub card_network: Option, + /// Payment method details from locker + #[cfg(feature = "payouts")] + #[schema(value_type = Option)] + pub bank_transfer: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, @@ -99,6 +109,19 @@ pub struct CardDetail { /// Card Holder's Nick Name #[schema(value_type = Option,example = "John Doe")] pub nick_name: Option>, + + /// Card Issuing Country + pub card_issuing_country: Option, + + /// Card's Network + #[schema(value_type = Option)] + pub card_network: Option, + + /// Issuer Bank for Card + pub card_issuer: Option, + + /// Card Type + pub card_type: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -116,7 +139,7 @@ pub struct PaymentMethodResponse { pub payment_method_id: String, /// The type of payment method use for the payment. - #[schema(value_type = PaymentMethodType, example = "card")] + #[schema(value_type = PaymentMethod, example = "card")] pub payment_method: api_enums::PaymentMethod, /// This is a sub-category of payment method. @@ -147,6 +170,11 @@ pub struct PaymentMethodResponse { #[schema(value_type = Option, example = "2023-01-18T11:04:09.922Z")] #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub created: Option, + + /// Payment method details from locker + #[cfg(feature = "payouts")] + #[schema(value_type = Option)] + pub bank_transfer: Option, } #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] @@ -162,6 +190,12 @@ pub struct CardDetailsPaymentMethod { pub expiry_year: Option>, pub nick_name: Option>, pub card_holder_name: Option>, + pub card_isin: Option, + pub card_issuer: Option, + pub card_network: Option, + pub card_type: Option, + #[serde(default = "saved_in_locker_default")] + pub saved_to_locker: bool, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -212,6 +246,18 @@ pub struct CardDetailFromLocker { #[schema(value_type=Option)] pub nick_name: Option>, + + #[schema(value_type = Option)] + pub card_network: Option, + + pub card_isin: Option, + pub card_issuer: Option, + pub card_type: Option, + pub saved_to_locker: bool, +} + +fn saved_in_locker_default() -> bool { + true } impl From for CardDetailFromLocker { @@ -227,6 +273,11 @@ impl From for CardDetailFromLocker { card_holder_name: item.card_holder_name, card_fingerprint: None, nick_name: item.nick_name, + card_isin: item.card_isin, + card_issuer: item.card_issuer, + card_network: item.card_network, + card_type: item.card_type, + saved_to_locker: item.saved_to_locker, } } } @@ -240,6 +291,11 @@ impl From for CardDetailsPaymentMethod { expiry_year: item.expiry_year, nick_name: item.nick_name, card_holder_name: item.card_holder_name, + card_isin: item.card_isin, + card_issuer: item.card_issuer, + card_network: item.card_network, + card_type: item.card_type, + saved_to_locker: item.saved_to_locker, } } } @@ -262,19 +318,6 @@ pub struct CardNetworkTypes { pub card_network: api_enums::CardNetwork, /// surcharge details for this card network - #[schema(example = r#" - { - "surcharge": { - "type": "rate", - "value": { - "percentage": 2.5 - } - }, - "tax_on_surcharge": { - "percentage": 1.5 - } - } - "#)] pub surcharge_details: Option, /// The list of eligible connectors for a given card network @@ -311,139 +354,59 @@ pub struct ResponsePaymentMethodTypes { pub required_fields: Option>, /// surcharge details for this payment method type if exists - #[schema(example = r#" - { - "surcharge": { - "type": "rate", - "value": { - "percentage": 2.5 - } - }, - "tax_on_surcharge": { - "percentage": 1.5 - } - } - "#)] 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)] + +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub struct SurchargeDetailsResponse { /// surcharge value - pub surcharge: Surcharge, + pub surcharge: SurchargeResponse, /// tax on surcharge value - pub tax_on_surcharge: Option>, + pub tax_on_surcharge: Option, /// surcharge amount for this payment - pub surcharge_amount: i64, + pub display_surcharge_amount: f64, /// tax on surcharge amount for this payment - pub tax_on_surcharge_amount: i64, + pub display_tax_on_surcharge_amount: f64, + /// sum of display_surcharge_amount and display_tax_on_surcharge_amount + pub display_total_surcharge_amount: f64, /// sum of original amount, - pub final_amount: i64, + pub display_final_amount: f64, } -impl SurchargeDetailsResponse { - pub fn is_request_surcharge_matching( - &self, - request_surcharge_details: RequestSurchargeDetails, - ) -> bool { - request_surcharge_details.surcharge_amount == self.surcharge_amount - && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount - } +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum SurchargeResponse { + /// Fixed Surcharge value + Fixed(i64), + /// Surcharge percentage + Rate(SurchargePercentage), } -#[derive(Clone, Debug)] -pub struct SurchargeMetadata { - surcharge_results: HashMap< - ( - common_enums::PaymentMethod, - common_enums::PaymentMethodType, - Option, - ), - SurchargeDetailsResponse, - >, - pub payment_attempt_id: String, -} - -impl SurchargeMetadata { - pub fn new(payment_attempt_id: String) -> Self { - Self { - surcharge_results: HashMap::new(), - payment_attempt_id, - } - } - pub fn is_empty_result(&self) -> bool { - self.surcharge_results.is_empty() - } - pub fn get_surcharge_results_size(&self) -> usize { - self.surcharge_results.len() - } - pub fn insert_surcharge_details( - &mut self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - surcharge_details: SurchargeDetailsResponse, - ) { - let key = ( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.insert(key, surcharge_details); - } - pub fn get_surcharge_details( - &self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> Option<&SurchargeDetailsResponse> { - let key = &( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.get(key) - } - pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { - format!("surcharge_metadata_{}", payment_attempt_id) - } - pub fn get_individual_surcharge_key_value_pairs( - &self, - ) -> Vec<(String, SurchargeDetailsResponse)> { - self.surcharge_results - .iter() - .map(|((pm, pmt, card_network), surcharge_details)| { - let key = - Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); - (key, surcharge_details.to_owned()) - }) - .collect() - } - pub fn get_surcharge_details_redis_hashset_key( - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> String { - if let Some(card_network) = card_network { - format!( - "{}_{}_{}", - payment_method, payment_method_type, card_network - ) - } else { - format!("{}_{}", payment_method, payment_method_type) +impl From for SurchargeResponse { + fn from(value: Surcharge) -> Self { + match value { + Surcharge::Fixed(amount) => Self::Fixed(amount), + Surcharge::Rate(percentage) => Self::Rate(percentage.into()), } } } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] -#[serde(rename_all = "snake_case", tag = "type", content = "value")] -pub enum Surcharge { - /// Fixed Surcharge value - Fixed(i64), - /// Surcharge percentage - Rate(Percentage), +#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +pub struct SurchargePercentage { + percentage: f32, } +impl From> for SurchargePercentage { + fn from(value: Percentage) -> Self { + Self { + percentage: value.get_percentage(), + } + } +} /// Required fields info used while listing the payment_method_data #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq, ToSchema, Hash)] pub struct RequiredFieldInfo { @@ -504,8 +467,11 @@ impl ResponsePaymentMethodIntermediate { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, PartialEq, Eq, Hash)] pub struct RequestPaymentMethodTypes { + #[schema(value_type = PaymentMethodType)] pub payment_method_type: api_enums::PaymentMethodType, + #[schema(value_type = Option)] pub payment_experience: Option, + #[schema(value_type = Option>)] pub card_networks: Option>, /// List of currencies accepted or has the processing capabilities of the processor #[schema(example = json!( @@ -513,7 +479,7 @@ pub struct RequestPaymentMethodTypes { "type": "specific_accepted", "list": ["USD", "INR"] } - ))] + ), value_type = Option)] pub accepted_currencies: Option, /// List of Countries accepted or has the processing capabilities of the processor @@ -522,7 +488,7 @@ pub struct RequestPaymentMethodTypes { "type": "specific_accepted", "list": ["UK", "AU"] } - ))] + ), value_type = Option)] pub accepted_countries: Option, /// Minimum amount supported by the processor. To be represented in the lowest denomination of the target currency (For example, for USD it should be in cents) @@ -547,8 +513,6 @@ pub struct RequestPaymentMethodTypes { #[derive(Debug, Clone, serde::Serialize, Default, ToSchema)] #[serde(deny_unknown_fields)] pub struct PaymentMethodListRequest { - #[serde(skip_deserializing)] - pub customer_id: Option, /// This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK #[schema(max_length = 30, min_length = 30, example = "secret_k2uj3he2893ein2d")] pub client_secret: Option, @@ -763,7 +727,7 @@ pub struct CustomerPaymentMethod { pub customer_id: String, /// The type of payment method use for the payment. - #[schema(value_type = PaymentMethodType,example = "card")] + #[schema(value_type = PaymentMethod,example = "card")] pub payment_method: api_enums::PaymentMethod, /// This is a sub-category of payment method. @@ -808,10 +772,23 @@ 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, + + /// Surcharge details for this saved card + pub surcharge_details: 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 d924fb2e4f62..a4f052549bd2 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -16,7 +16,6 @@ use crate::{ admin, disputes, enums::{self as api_enums}, ephemeral_key::EphemeralKeyCreateResponse, - payment_methods::{Surcharge, SurchargeDetailsResponse}, refunds, }; @@ -83,9 +82,25 @@ pub struct CustomerDetails { ToSchema, router_derive::PolymorphicSchema, )] -#[generate_schemas(PaymentsCreateRequest)] +#[generate_schemas(PaymentsCreateRequest, PaymentsUpdateRequest, PaymentsConfirmRequest)] #[serde(deny_unknown_fields)] pub struct PaymentsRequest { + /// The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and ¥100 since ¥ is a zero-decimal currency + #[schema(value_type = Option, example = 6540)] + #[serde(default, deserialize_with = "amount::deserialize_option")] + #[mandatory_in(PaymentsCreateRequest = u64)] + // Makes the field mandatory in PaymentsCreateRequest + pub amount: Option, + + /// The three letter ISO currency code in uppercase. Eg: 'USD' to charge US Dollars + #[schema(example = "USD", value_type = Option)] + #[mandatory_in(PaymentsCreateRequest = Currency)] + pub currency: Option, + + /// The Amount to be captured / debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, the default amount_to_capture will be the payment amount. + #[schema(example = 6540)] + pub amount_to_capture: Option, + /// Unique identifier for the payment. This ensures idempotency for multiple payments /// that have been done by a single merchant. This field is auto generated and is returned in the API response. #[schema( @@ -102,36 +117,26 @@ pub struct PaymentsRequest { #[schema(max_length = 255, example = "merchant_1668273825")] pub merchant_id: Option, - /// The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., - #[schema(value_type = Option, example = 6540)] - #[serde(default, deserialize_with = "amount::deserialize_option")] - #[mandatory_in(PaymentsCreateRequest)] - // Makes the field mandatory in PaymentsCreateRequest - pub amount: Option, - - #[schema(value_type = Option, example = json!({ + #[schema(value_type = Option, example = json!({ "type": "single", - "data": "stripe" + "data": {"connector": "stripe", "merchant_connector_id": "mca_123"} }))] pub routing: Option, - /// This allows the merchant to manually select a connector with which the payment can go through + /// This allows to manually select a connector with which the payment can go through #[schema(value_type = Option>, max_length = 255, example = json!(["stripe", "adyen"]))] pub connector: Option>, - /// The currency of the payment request can be specified here - #[schema(value_type = Option, example = "USD")] - #[mandatory_in(PaymentsCreateRequest)] - pub currency: Option, - - /// This is the instruction for capture/ debit the money from the users' card. On the other hand authorization refers to blocking the amount on the users' payment method. + /// Default value if not passed is set to 'automatic' which results in Auth and Capture in one single API request. Pass 'manual' or 'manual_multiple' in case you want do a separate Auth and Capture by first authorizing and placing a hold on your customer's funds so that you can use the Payments/Capture endpoint later to capture the authorized amount. Pass 'manual' if you want to only capture the amount later once or 'manual_multiple' if you want to capture the funds multiple times later. Both 'manual' and 'manual_multiple' are only supported by a specific list of processors #[schema(value_type = Option, example = "automatic")] pub capture_method: Option, - /// The Amount to be captured/ debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., - /// If not provided, the default amount_to_capture will be the payment amount. - #[schema(example = 6540)] - pub amount_to_capture: Option, + /// Pass this parameter to force 3DS or non 3DS auth for this payment. Some connectors will still force 3DS auth even in case of passing 'no_three_ds' here and vice versa. Default value is 'no_three_ds' if not set + #[schema(value_type = Option, example = "no_three_ds", default = "three_ds")] + pub authentication_type: Option, + + /// The billing details of the customer + pub billing: Option
, /// A timestamp (ISO 8601 code) that determines when the payment should be captured. /// Providing this field will automatically set `capture` to true @@ -143,23 +148,19 @@ pub struct PaymentsRequest { #[schema(default = false, example = true)] pub confirm: Option, - /// The details of a customer for this payment - /// This will create the customer if `customer.id` does not exist - /// If customer id already exists, it will update the details of the customer + /// Passing this object creates a new customer or attaches an existing customer to the payment pub customer: Option, - /// The identifier for the customer object. - /// This field will be deprecated soon, use the customer object instead + /// The identifier for the customer object. This field will be deprecated soon, use the customer object instead #[schema(max_length = 255, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub customer_id: Option, - /// The customer's email address - /// This field will be deprecated soon, use the customer object instead + /// The customer's email address This field will be deprecated soon, use the customer object instead #[schema(max_length = 255, value_type = Option, example = "johntest@test.com")] pub email: Option, - /// description: The customer's name - /// This field will be deprecated soon, use the customer object instead + /// The customer's name. + /// This field will be deprecated soon, use the customer object instead. #[schema(value_type = Option, max_length = 255, example = "John Test")] pub name: Option>, @@ -173,11 +174,11 @@ pub struct PaymentsRequest { #[schema(max_length = 255, example = "+1")] pub phone_country_code: Option, - /// Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with `confirm: true`. + /// Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. When making a recurring payment by passing a mandate_id, this parameter is mandatory #[schema(example = true)] pub off_session: Option, - /// A description of the payment + /// A description for the payment #[schema(example = "It's my first payment request")] pub description: Option, @@ -188,10 +189,6 @@ pub struct PaymentsRequest { #[schema(value_type = Option, example = "off_session")] pub setup_future_usage: Option, - /// The transaction authentication can be set to undergo payer authentication. - #[schema(value_type = Option, example = "no_three_ds", default = "three_ds")] - pub authentication_type: Option, - /// The payment method information provided for making a payment #[schema(example = "bank_transfer")] pub payment_method_data: Option, @@ -204,16 +201,13 @@ pub struct PaymentsRequest { #[schema(example = "187282ab-40ef-47a9-9206-5099ba31e432")] pub payment_token: Option, - /// This is used when payment is to be confirmed and the card is not saved - #[schema(value_type = Option)] + /// This is used along with the payment_token field while collecting during saved card payments. This field will be deprecated soon, use the payment_method_data.card_token object instead + #[schema(value_type = Option, deprecated)] pub card_cvc: Option>, /// The shipping address for the payment pub shipping: Option
, - /// The billing address for the payment - pub billing: Option
, - /// For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters. #[schema(max_length = 255, example = "Hyperswitch Router")] pub statement_descriptor_name: Option, @@ -222,28 +216,30 @@ pub struct PaymentsRequest { #[schema(max_length = 255, example = "Payment for shoes purchase")] pub statement_descriptor_suffix: Option, - /// Information about the product , quantity and amount for connectors. (e.g. Klarna) + /// Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount #[schema(value_type = Option>, example = r#"[{ - "product_name": "gillete creme", - "quantity": 15, - "amount" : 900 + "product_name": "Apple iPhone 16", + "quantity": 1, + "amount" : 69000 "product_img_link" : "https://dummy-img-link.com" }]"#)] pub order_details: Option>, /// It's a token used for client side verification. #[schema(example = "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo")] + #[remove_in(PaymentsUpdateRequest, PaymentsCreateRequest)] pub client_secret: Option, - /// Provide mandate information for creating a mandate + /// Passing this object during payments creates a mandate. The mandate_type sub object is passed by the server usually and the customer_acceptance sub object is usually passed by the SDK or client pub mandate_data: Option, - /// A unique identifier to link the payment to a mandate, can be use instead of payment_method_data + /// A unique identifier to link the payment to a mandate. To do Recurring payments after a mandate has been created, pass the mandate_id instead of payment_method_data #[schema(max_length = 255, example = "mandate_iwer89rnjef349dni3")] + #[remove_in(PaymentsUpdateRequest)] pub mandate_id: Option, /// Additional details required by 3DS 2.0 - #[schema(value_type = Option, example = r#"{ + #[schema(value_type = Option, example = r#"{ "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", @@ -256,7 +252,7 @@ pub struct PaymentsRequest { }"#)] pub browser_info: Option, - /// Payment Experience for the current payment + /// To indicate the type of payment experience that the payment method would go through #[schema(value_type = Option, example = "redirect_to_url")] pub payment_experience: Option, @@ -264,23 +260,28 @@ pub struct PaymentsRequest { #[schema(value_type = Option, example = "google_pay")] pub payment_method_type: Option, - /// Business country of the merchant for this payment + /// Business country of the merchant for this payment. + /// To be deprecated soon. Pass the profile_id instead #[schema(value_type = Option, example = "US")] + #[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest)] pub business_country: Option, - /// Business label of the merchant for this payment + /// Business label of the merchant for this payment. + /// To be deprecated soon. Pass the profile_id instead #[schema(example = "food")] + #[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest)] pub business_label: Option, /// Merchant connector details used to make payments. #[schema(value_type = Option)] pub merchant_connector_details: Option, - /// Allowed Payment Method Types for a given PaymentIntent + /// Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent #[schema(value_type = Option>)] pub allowed_payment_method_types: Option>, /// Business sub label for the payment + #[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest, PaymentsCreateRequest)] pub business_sub_label: Option, /// Denotes the retry action @@ -296,22 +297,51 @@ pub struct PaymentsRequest { /// additional data that might be required by hyperswitch pub feature_metadata: Option, - /// payment link object required for generating the payment_link - pub payment_link_object: Option, + + /// Whether to get the payment link (if applicable) + #[schema(default = false, example = true)] + pub payment_link: Option, + + /// custom payment link config for the particular payment + #[schema(value_type = Option)] + pub payment_link_config: Option, /// The business profile to use for this payment, if not passed the default business profile /// associated with the merchant account will be used. + #[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest)] pub profile_id: Option, /// surcharge_details for this payment + #[remove_in(PaymentsConfirmRequest)] #[schema(value_type = Option)] pub surcharge_details: Option, /// 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, + + ///Will be used to expire client secret after certain amount of time to be supplied in seconds + ///(900) for 15 mins + #[schema(example = 900)] + pub session_expiry: Option, + + /// additional data related to some frm connectors + pub frm_metadata: Option, } +impl PaymentsRequest { + pub fn get_total_capturable_amount(&self) -> Option { + let surcharge_amount = self + .surcharge_details + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + .unwrap_or(0); + self.amount + .map(|amount| i64::from(amount) + surcharge_amount) + } +} #[derive( Default, Debug, Clone, serde::Serialize, serde::Deserialize, Copy, ToSchema, PartialEq, )] @@ -320,20 +350,50 @@ pub struct RequestSurchargeDetails { pub tax_amount: Option, } +/// Browser information to be used for 3DS 2.0 +#[derive(ToSchema)] +pub struct BrowserInformation { + /// Color depth supported by the browser + pub color_depth: Option, + + /// Whether java is enabled in the browser + pub java_enabled: Option, + + /// Whether javascript is enabled in the browser + pub java_script_enabled: Option, + + /// Language supported + pub language: Option, + + /// The screen height in pixels + pub screen_height: Option, + + /// The screen width in pixels + pub screen_width: Option, + + /// Time zone of the client + pub time_zone: Option, + + /// Ip address of the client + #[schema(value_type = Option)] + pub ip_address: Option, + + /// List of headers that are accepted + #[schema( + example = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" + )] + pub accept_header: Option, + + /// User-agent of the browser + pub user_agent: Option, +} + impl RequestSurchargeDetails { pub fn is_surcharge_zero(&self) -> bool { self.surcharge_amount == 0 && self.tax_amount.unwrap_or(0) == 0 } - pub fn get_surcharge_details_object(&self, original_amount: i64) -> SurchargeDetailsResponse { - let surcharge_amount = self.surcharge_amount; - let tax_on_surcharge_amount = self.tax_amount.unwrap_or(0); - SurchargeDetailsResponse { - surcharge: Surcharge::Fixed(self.surcharge_amount), - tax_on_surcharge: None, - surcharge_amount, - tax_on_surcharge_amount, - 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) } } @@ -343,6 +403,15 @@ pub struct HeaderPayload { pub x_hs_latency: Option, } +impl HeaderPayload { + pub fn with_source(payment_confirm_source: api_enums::PaymentSource) -> Self { + Self { + payment_confirm_source: Some(payment_confirm_source), + ..Default::default() + } + } +} + #[derive( Default, Debug, serde::Serialize, Clone, PartialEq, ToSchema, router_derive::PolymorphicSchema, )] @@ -391,6 +460,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( @@ -553,10 +626,18 @@ pub enum MandateReferenceId { NetworkMandateId(String), // network_txns_id send by Issuer to connector, Used for PG agnostic mandate txns } -#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)] pub struct ConnectorMandateReferenceId { pub connector_mandate_id: Option, pub payment_method_id: Option, + pub update_history: Option>, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct UpdateHistory { + pub connector_mandate_id: Option, + pub payment_method_id: String, + pub original_payment_id: Option, } impl MandateIds { @@ -573,6 +654,8 @@ impl MandateIds { #[derive(Default, Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct MandateData { + /// A way to update the mandate's payment method details + pub update_mandate_id: Option, /// A concent from the customer to store the payment method pub customer_acceptance: Option, /// A way to select the type of mandate used @@ -639,6 +722,7 @@ pub struct CustomerAcceptance { #[derive(Default, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq, Clone, ToSchema)] #[serde(rename_all = "lowercase")] +/// This is used to indicate if the mandate was accepted online or offline pub enum AcceptanceType { Online, #[default] @@ -671,7 +755,7 @@ pub struct Card { /// The card holder's name #[schema(value_type = String, example = "John Test")] - pub card_holder_name: Secret, + pub card_holder_name: Option>, /// The CVC number for the card #[schema(value_type = String, example = "242")] @@ -698,6 +782,45 @@ pub struct Card { pub nick_name: Option>, } +impl Card { + fn apply_additional_card_info(&self, additional_card_info: AdditionalCardInfo) -> Self { + Self { + card_number: self.card_number.clone(), + card_exp_month: self.card_exp_month.clone(), + card_exp_year: self.card_exp_year.clone(), + card_holder_name: self.card_holder_name.clone(), + card_cvc: self.card_cvc.clone(), + card_issuer: self + .card_issuer + .clone() + .or(additional_card_info.card_issuer), + card_network: self + .card_network + .clone() + .or(additional_card_info.card_network), + card_type: self.card_type.clone().or(additional_card_info.card_type), + card_issuing_country: self + .card_issuing_country + .clone() + .or(additional_card_info.card_issuing_country), + bank_code: self.bank_code.clone().or(additional_card_info.bank_code), + nick_name: self.nick_name.clone(), + } + } +} + +#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema, Default)] +#[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>, + + /// The CVC number for the card + #[schema(value_type = Option)] + pub card_cvc: Option>, +} + #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum CardRedirectData { @@ -814,19 +937,34 @@ pub enum BankDebitData { #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum PaymentMethodData { + #[schema(title = "Card")] Card(Card), + #[schema(title = "CardRedirect")] CardRedirect(CardRedirectData), + #[schema(title = "Wallet")] Wallet(WalletData), + #[schema(title = "PayLater")] PayLater(PayLaterData), + #[schema(title = "BankRedirect")] BankRedirect(BankRedirectData), + #[schema(title = "BankDebit")] BankDebit(BankDebitData), + #[schema(title = "BankTransfer")] BankTransfer(Box), + #[schema(title = "Crypto")] Crypto(CryptoData), + #[schema(title = "MandatePayment")] MandatePayment, + #[schema(title = "Reward")] Reward, + #[schema(title = "Upi")] Upi(UpiData), + #[schema(title = "Voucher")] Voucher(VoucherData), + #[schema(title = "GiftCard")] GiftCard(Box), + #[schema(title = "CardToken")] + CardToken(CardToken), } impl PaymentMethodData { @@ -854,7 +992,23 @@ impl PaymentMethodData { | Self::Reward | Self::Upi(_) | Self::Voucher(_) - | Self::GiftCard(_) => None, + | Self::GiftCard(_) + | Self::CardToken(_) => None, + } + } + pub fn apply_additional_payment_data( + &self, + additional_payment_data: AdditionalPaymentData, + ) -> Self { + if let AdditionalPaymentData::Card(additional_card_info) = additional_payment_data { + match self { + Self::Card(card) => { + Self::Card(card.apply_additional_card_info(*additional_card_info)) + } + _ => self.to_owned(), + } + } else { + self.to_owned() } } } @@ -1050,6 +1204,7 @@ pub struct AdditionalCardInfo { pub bank_code: Option, pub last4: Option, pub card_isin: Option, + pub card_extended_bin: Option, pub card_exp_month: Option>, pub card_exp_year: Option>, pub card_holder_name: Option>, @@ -1073,6 +1228,7 @@ pub enum AdditionalPaymentData { GiftCard {}, Voucher {}, CardRedirect {}, + CardToken {}, } #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] @@ -1104,7 +1260,7 @@ pub enum BankRedirectData { }, Eps { /// The billing details for bank redirection - billing_details: BankRedirectBilling, + billing_details: Option, /// The hyperswitch bank code for eps #[schema(value_type = BankNames, example = "triodos_bank")] @@ -1133,7 +1289,7 @@ pub enum BankRedirectData { }, Ideal { /// The billing details for bank redirection - billing_details: BankRedirectBilling, + billing_details: Option, /// The hyperswitch bank code for ideal #[schema(value_type = BankNames, example = "abn_amro")] @@ -1174,10 +1330,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 @@ -1189,15 +1345,15 @@ pub enum BankRedirectData { }, Sofort { /// The billing details for bank redirection - billing_details: BankRedirectBilling, + billing_details: Option, /// The country for bank payment #[schema(value_type = CountryAlpha2, example = "US")] - country: api_enums::CountryAlpha2, + country: Option, /// The preferred language #[schema(example = "en")] - preferred_language: String, + preferred_language: Option, }, Trustly { /// The country for bank payment @@ -1585,6 +1741,7 @@ pub struct CardResponse { pub card_issuer: Option, pub card_issuing_country: Option, pub card_isin: Option, + pub card_extended_bin: Option, pub card_exp_month: Option>, pub card_exp_year: Option>, pub card_holder_name: Option>, @@ -1627,7 +1784,7 @@ pub enum VoucherData { #[serde(rename_all = "snake_case")] pub enum PaymentMethodDataResponse { #[serde(rename = "card")] - Card(CardResponse), + Card(Box), BankTransfer, Wallet, PayLater, @@ -1641,6 +1798,7 @@ pub enum PaymentMethodDataResponse { Voucher, GiftCard, CardRedirect, + CardToken, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -1675,6 +1833,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()) @@ -1691,6 +1863,7 @@ pub struct Address { } // used by customers also, could be moved outside +/// Address details #[derive(Clone, Default, Debug, Eq, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] #[serde(deny_unknown_fields)] pub struct AddressDetails { @@ -1795,8 +1968,12 @@ pub enum NextActionData { /// Contains url for Qr code image, this qr code has to be shown in sdk QrCodeInformation { #[schema(value_type = String)] - image_data_url: Url, + /// Hyperswitch generated image data source url + image_data_url: Option, display_to_timestamp: Option, + #[schema(value_type = String)] + /// The url for Qr code given by the connector + qr_code_url: Option, }, /// Contains the download url and the reference number for transaction DisplayVoucherInformation { @@ -1810,6 +1987,26 @@ pub enum NextActionData { }, } +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +// the enum order shouldn't be changed as this is being used during serialization and deserialization +pub enum QrCodeInformation { + QrCodeUrl { + image_data_url: Url, + qr_code_url: Url, + display_to_timestamp: Option, + }, + QrDataUrl { + image_data_url: Url, + display_to_timestamp: Option, + }, + QrCodeImageUrl { + qr_code_url: Url, + display_to_timestamp: Option, + }, +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct BankTransferNextStepsData { /// The instructions for performing a bank transfer @@ -1822,7 +2019,7 @@ pub struct BankTransferNextStepsData { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct VoucherNextStepData { /// Voucher expiry date and time - pub expires_at: Option, + pub expires_at: Option, /// Reference number required for the transaction pub reference: String, /// Url to download the payment instruction @@ -1835,6 +2032,7 @@ pub struct VoucherNextStepData { pub struct QrCodeNextStepsInstruction { pub image_data_url: Url, pub display_to_timestamp: Option, + pub qr_code_url: Option, } #[derive(Clone, Debug, serde::Deserialize)] @@ -1889,8 +2087,8 @@ pub struct MultibancoTransferInstructions { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct DokuBankTransferInstructions { - #[schema(value_type = String, example = "2023-07-26T17:33:00-07-21")] - pub expires_at: Option, + #[schema(value_type = String, example = "1707091200000")] + pub expires_at: Option, #[schema(value_type = String, example = "122385736258")] pub reference: Secret, #[schema(value_type = String)] @@ -1942,6 +2140,11 @@ pub struct PaymentsResponse { #[schema(example = 100)] pub amount: i64, + /// The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount, + /// If no surcharge_details, net_amount = amount + #[schema(example = 110)] + pub net_amount: i64, + /// The maximum amount that could be captured from the payment #[schema(minimum = 100, example = 6540)] pub amount_capturable: Option, @@ -2089,6 +2292,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, @@ -2159,6 +2368,23 @@ 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, + + /// Total number of authorizations happened in an incremental_authorization payment + pub authorization_count: Option, + + /// List of incremental authorizations happened to the payment + pub incremental_authorizations: Option>, + + /// Date Time expiry of the payment + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub expires_on: Option, + + /// Payment Fingerprint + pub fingerprint: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -2227,6 +2453,24 @@ pub struct PaymentListResponse { // The list of payments response objects pub data: Vec, } + +#[derive(Setter, Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +pub struct IncrementalAuthorizationResponse { + /// The unique identifier of authorization + pub authorization_id: String, + /// Amount the authorization has been made for + pub amount: i64, + #[schema(value_type= AuthorizationStatus)] + /// The status of the authorization + pub status: common_enums::AuthorizationStatus, + /// Error code sent by the connector for authorization + pub error_code: Option, + /// Error message sent by the connector for authorization + pub error_message: Option, + /// Previously authorized amount for the payment + pub previously_authorized_amount: i64, +} + #[derive(Clone, Debug, serde::Serialize)] pub struct PaymentListResponseV2 { /// The number of payments included in the list for given constraints @@ -2288,9 +2532,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, } @@ -2393,6 +2639,7 @@ impl From for CardResponse { card_issuer: card.card_issuer, card_issuing_country: card.card_issuing_country, card_isin: card.card_isin, + card_extended_bin: card.card_extended_bin, card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, card_holder_name: card.card_holder_name, @@ -2403,7 +2650,7 @@ impl From for CardResponse { impl From for PaymentMethodDataResponse { fn from(payment_method_data: AdditionalPaymentData) -> Self { match payment_method_data { - AdditionalPaymentData::Card(card) => Self::Card(CardResponse::from(*card)), + AdditionalPaymentData::Card(card) => Self::Card(Box::new(CardResponse::from(*card))), AdditionalPaymentData::PayLater {} => Self::PayLater, AdditionalPaymentData::Wallet {} => Self::Wallet, AdditionalPaymentData::BankRedirect { .. } => Self::BankRedirect, @@ -2416,6 +2663,7 @@ impl From for PaymentMethodDataResponse { AdditionalPaymentData::Voucher {} => Self::Voucher, AdditionalPaymentData::GiftCard {} => Self::GiftCard, AdditionalPaymentData::CardRedirect {} => Self::CardRedirect, + AdditionalPaymentData::CardToken {} => Self::CardToken, } } } @@ -2479,8 +2727,30 @@ pub struct OrderDetailsWithAmount { pub quantity: u16, /// the amount per quantity of product pub amount: i64, + // Does the order includes shipping + pub requires_shipping: Option, /// The image URL of the product pub product_img_link: Option, + /// ID of the product that is being purchased + pub product_id: Option, + /// Category of the product that is being purchased + pub category: Option, + /// Brand of the product that is being purchased + pub brand: Option, + /// Type of the product that is being purchased + pub product_type: Option, +} + +#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProductType { + #[default] + Physical, + Digital, + Travel, + Ride, + Event, + Accommodation, } #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -2491,8 +2761,18 @@ pub struct OrderDetails { /// The quantity of the product to be purchased #[schema(example = 1)] pub quantity: u16, + // Does the order include shipping + pub requires_shipping: Option, /// The image URL of the product pub product_img_link: Option, + /// ID of the product that is being purchased + pub product_id: Option, + /// Category of the product that is being purchased + pub category: Option, + /// Brand of the product that is being purchased + pub brand: Option, + /// Type of the product that is being purchased + pub product_type: Option, } #[derive(Default, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -2852,7 +3132,7 @@ pub struct SecretInfoToInitiateSdk { pub struct ApplePayPaymentRequest { /// The code for country #[schema(value_type = CountryAlpha2, example = "US")] - pub country_code: api_enums::CountryAlpha2, + pub country_code: Option, /// The code for currency #[schema(value_type = Currency, example = "USD")] pub currency_code: api_enums::Currency, @@ -2928,10 +3208,22 @@ pub struct PaymentsCancelRequest { /// The reason for the payment cancel pub cancellation_reason: Option, /// Merchant connector details used to make payments. - #[schema(value_type = MerchantConnectorDetailsWrap)] + #[schema(value_type = Option)] pub merchant_connector_details: Option, } +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] +pub struct PaymentsIncrementalAuthorizationRequest { + /// The identifier for the payment + #[serde(skip)] + pub payment_id: String, + /// The total amount including previously authorized amount and additional amount + #[schema(value_type = i64, example = 6540)] + pub amount: i64, + /// Reason for incremental authorization + pub reason: Option, +} + #[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct PaymentsApproveRequest { /// The identifier for the payment @@ -3145,15 +3437,6 @@ mod tests { } } -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] -pub struct PaymentLinkObject { - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub link_expiry: Option, - pub merchant_custom_domain_name: Option, - /// Custom merchant name for payment link - pub custom_merchant_name: Option, -} - #[derive(Default, Debug, serde::Deserialize, Clone, ToSchema, serde::Serialize)] pub struct RetrievePaymentLinkRequest { pub client_secret: Option, @@ -3168,18 +3451,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, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub expiry: Option, + pub description: Option, + pub status: PaymentLinkStatus, + #[schema(value_type = Option)] + pub currency: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -3188,19 +3470,135 @@ pub struct PaymentLinkInitiateRequest { pub payment_id: String, } +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum PaymentLinkData { + PaymentLinkDetails(PaymentLinkDetails), + PaymentLinkStatusDetails(PaymentLinkStatusDetails), +} + #[derive(Debug, serde::Serialize)] pub struct PaymentLinkDetails { - pub amount: i64, + pub amount: String, pub currency: api_enums::Currency, pub pub_key: String, pub client_secret: String, pub payment_id: String, - #[serde(with = "common_utils::custom_serde::iso8601::option")] - pub expiry: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub session_expiry: PrimitiveDateTime, pub merchant_logo: String, pub return_url: String, pub merchant_name: String, - pub order_details: Option>, + pub order_details: Option>, pub max_items_visible_after_collapse: i8, - pub sdk_theme: Option, + pub theme: String, + pub merchant_description: Option, + pub sdk_layout: String, +} + +#[derive(Debug, serde::Serialize)] +pub struct PaymentLinkStatusDetails { + pub amount: String, + pub currency: api_enums::Currency, + pub payment_id: String, + pub merchant_logo: String, + pub merchant_name: String, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created: PrimitiveDateTime, + pub status: PaymentLinkStatusWrap, + pub error_code: Option, + pub error_message: Option, + pub redirect: bool, + pub theme: String, + pub return_url: String, +} + +#[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, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] +pub struct PaymentCreatePaymentLinkConfig { + #[serde(flatten)] + #[schema(value_type = Option)] + pub config: admin::PaymentLinkConfigRequest, +} + +#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +pub struct OrderDetailsWithStringAmount { + /// Name of the product that is being purchased + #[schema(max_length = 255, example = "shirt")] + pub product_name: String, + /// The quantity of the product to be purchased + #[schema(example = 1)] + pub quantity: u16, + /// the amount per quantity of product + pub amount: String, + /// Product Image link + pub product_img_link: Option, +} + +#[derive(PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum PaymentLinkStatus { + Active, + Expired, +} + +#[derive(PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum PaymentLinkStatusWrap { + PaymentLinkStatus(PaymentLinkStatus), + IntentStatus(api_enums::IntentStatus), } diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 5cc5e5118166..9e771b471214 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -181,7 +181,7 @@ pub struct Card { /// The card holder's name #[schema(value_type = String, example = "John Doe")] - pub card_holder_name: Secret, + pub card_holder_name: Option>, } #[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] @@ -195,16 +195,16 @@ pub enum Bank { #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct AchBankTransfer { /// Bank name - #[schema(value_type = String, example = "Deutsche Bank")] - pub bank_name: String, + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, /// Bank country code - #[schema(value_type = CountryAlpha2, example = "US")] - pub bank_country_code: api_enums::CountryAlpha2, + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, /// Bank city - #[schema(value_type = String, example = "California")] - pub bank_city: String, + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, /// Bank account number is an unique identifier assigned by a bank to a customer. #[schema(value_type = String, example = "000123456")] @@ -218,16 +218,16 @@ pub struct AchBankTransfer { #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct BacsBankTransfer { /// Bank name - #[schema(value_type = String, example = "Deutsche Bank")] - pub bank_name: String, + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, /// Bank country code - #[schema(value_type = CountryAlpha2, example = "US")] - pub bank_country_code: api_enums::CountryAlpha2, + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, /// Bank city - #[schema(value_type = String, example = "California")] - pub bank_city: String, + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, /// Bank account number is an unique identifier assigned by a bank to a customer. #[schema(value_type = String, example = "000123456")] @@ -242,16 +242,16 @@ pub struct BacsBankTransfer { // The SEPA (Single Euro Payments Area) is a pan-European network that allows you to send and receive payments in euros between two cross-border bank accounts in the eurozone. pub struct SepaBankTransfer { /// Bank name - #[schema(value_type = String, example = "Deutsche Bank")] - pub bank_name: String, + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, /// Bank country code - #[schema(value_type = CountryAlpha2, example = "US")] - pub bank_country_code: api_enums::CountryAlpha2, + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, /// Bank city - #[schema(value_type = String, example = "California")] - pub bank_city: String, + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, /// International Bank Account Number (iban) - used in many countries for identifying a bank along with it's customer. #[schema(value_type = String, example = "DE89370400440532013000")] @@ -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/pm_auth.rs b/crates/api_models/src/pm_auth.rs new file mode 100644 index 000000000000..7044bd8d3352 --- /dev/null +++ b/crates/api_models/src/pm_auth.rs @@ -0,0 +1,57 @@ +use common_enums::{PaymentMethod, PaymentMethodType}; +use common_utils::{ + events::{ApiEventMetric, ApiEventsType}, + impl_misc_api_event_type, +}; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub struct LinkTokenCreateRequest { + pub language: Option, // optional language field to be passed + pub client_secret: Option, // client secret to be passed in req body + pub payment_id: String, // payment_id to be passed in req body for redis pm_auth connector name fetch + pub payment_method: PaymentMethod, // payment_method to be used for filtering pm_auth connector + pub payment_method_type: PaymentMethodType, // payment_method_type to be used for filtering pm_auth connector +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct LinkTokenCreateResponse { + pub link_token: String, // link_token received in response + pub connector: String, // pm_auth connector name in response +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] + +pub struct ExchangeTokenCreateRequest { + pub public_token: String, + pub client_secret: Option, + pub payment_id: String, + pub payment_method: PaymentMethod, + pub payment_method_type: PaymentMethodType, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ExchangeTokenCreateResponse { + pub access_token: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentMethodAuthConfig { + pub enabled_payment_methods: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentMethodAuthConnectorChoice { + pub payment_method: PaymentMethod, + pub payment_method_type: PaymentMethodType, + pub connector_name: String, + pub mca_id: String, +} + +impl_misc_api_event_type!( + LinkTokenCreateRequest, + LinkTokenCreateResponse, + ExchangeTokenCreateRequest, + ExchangeTokenCreateResponse +); diff --git a/crates/api_models/src/recon.rs b/crates/api_models/src/recon.rs new file mode 100644 index 000000000000..efbe28f96ba4 --- /dev/null +++ b/crates/api_models/src/recon.rs @@ -0,0 +1,21 @@ +use common_utils::pii; +use masking::Secret; + +use crate::enums; + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ReconUpdateMerchantRequest { + pub merchant_id: String, + pub recon_status: enums::ReconStatus, + pub user_email: pii::Email, +} + +#[derive(Debug, serde::Serialize)] +pub struct ReconTokenResponse { + pub token: Secret, +} + +#[derive(Debug, serde::Serialize)] +pub struct ReconStatusResponse { + pub recon_status: enums::ReconStatus, +} diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 6fe8be8b5291..97182df0a5e5 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -9,21 +9,21 @@ use crate::{admin, enums}; #[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct RefundRequest { - /// Unique Identifier for the Refund. This is to ensure idempotency for multiple partial refund initiated against the same payment. If the identifiers is not defined by the merchant, this filed shall be auto generated and provide in the API response. It is recommended to generate uuid(v4) as the refund_id. + /// The payment id against which refund is to be intiated #[schema( max_length = 30, min_length = 30, - example = "ref_mbabizu24mvu3mela5njyhpit4" + example = "pay_mbabizu24mvu3mela5njyhpit4" )] - pub refund_id: Option, + pub payment_id: String, - /// Total amount for which the refund is to be initiated. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc. If not provided, this will default to the full payment amount + /// Unique Identifier for the Refund. This is to ensure idempotency for multiple partial refunds initiated against the same payment. If this is not passed by the merchant, this field shall be auto generated and provided in the API response. It is recommended to generate uuid(v4) as the refund_id. #[schema( max_length = 30, min_length = 30, - example = "pay_mbabizu24mvu3mela5njyhpit4" + example = "ref_mbabizu24mvu3mela5njyhpit4" )] - pub payment_id: String, + pub refund_id: Option, /// The identifier for the Merchant Account #[schema(max_length = 255, example = "y3oqhf46pyzuxjbcn2giaqnb44")] @@ -33,11 +33,11 @@ pub struct RefundRequest { #[schema(minimum = 100, example = 6540)] pub amount: Option, - /// An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive + /// Reason for the refund. Often useful for displaying to users and your customer support executive. In case the payment went through Stripe, this field needs to be passed with one of these enums: `duplicate`, `fraudulent`, or `requested_by_customer` #[schema(max_length = 255, example = "Customer returned the product")] pub reason: Option, - /// The type of refund based on waiting time for processing: Scheduled or Instant Refund + /// To indicate whether to refund needs to be instant or scheduled. Default value is instant #[schema(default = "Instant", example = "Instant")] pub refund_type: Option, @@ -87,6 +87,7 @@ pub struct RefundUpdateRequest { pub metadata: Option, } +/// To indicate whether to refund needs to be instant or scheduled #[derive( Default, Debug, Clone, Copy, ToSchema, Deserialize, Serialize, Eq, PartialEq, strum::Display, )] @@ -99,18 +100,18 @@ pub enum RefundType { #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)] pub struct RefundResponse { - /// The identifier for refund + /// Unique Identifier for the refund pub refund_id: String, - /// The identifier for payment + /// The payment id against which refund is intiated pub payment_id: String, /// The refund amount, which should be less than or equal to the total payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc pub amount: i64, /// The three-letter ISO currency code pub currency: String, - /// An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive - pub reason: Option, /// The status for refund pub status: RefundStatus, + /// An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive + pub reason: Option, /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object #[schema(value_type = Option)] pub metadata: Option, @@ -127,7 +128,10 @@ pub struct RefundResponse { /// The connector used for the refund and the corresponding payment #[schema(example = "stripe")] pub connector: String, + /// The id of business profile for this refund pub profile_id: Option, + /// The merchant_connector_id of the processor through which this payment went through + pub merchant_connector_id: Option, } #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)] @@ -174,7 +178,7 @@ pub struct RefundListMetaData { pub currency: Vec, /// The list of available refund status filters #[schema(value_type = Vec)] - pub status: Vec, + pub refund_status: Vec, } /// The status for refunds diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 47a44ea7443e..2775034c88c0 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -2,19 +2,19 @@ use std::fmt::Debug; use common_utils::errors::ParsingError; use error_stack::IntoReport; -use euclid::{ +pub use euclid::{ dssa::types::EuclidAnalysable, - enums as euclid_enums, frontend::{ ast, dir::{DirKeyKind, EuclidDirFilter}, }, }; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use crate::enums::{self, RoutableConnectors}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(tag = "type", content = "data", rename_all = "snake_case")] pub enum ConnectorSelection { Priority(Vec), @@ -32,7 +32,7 @@ impl ConnectorSelection { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct RoutingConfigRequest { pub name: Option, pub description: Option, @@ -40,7 +40,7 @@ pub struct RoutingConfigRequest { pub profile_id: Option, } -#[derive(Debug, serde::Serialize)] +#[derive(Debug, serde::Serialize, ToSchema)] pub struct ProfileDefaultRoutingConfig { pub profile_id: String, pub connectors: Vec, @@ -61,19 +61,21 @@ pub struct RoutingRetrieveLinkQuery { pub profile_id: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +/// Response of the retrieved routing configs for a merchant account pub struct RoutingRetrieveResponse { pub algorithm: Option, } -#[derive(Debug, serde::Serialize)] +#[derive(Debug, serde::Serialize, ToSchema)] #[serde(untagged)] pub enum LinkedRoutingConfigRetrieveResponse { MerchantAccountBased(RoutingRetrieveResponse), ProfileBased(Vec), } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +/// Routing Algorithm specific to merchants pub struct MerchantRoutingAlgorithm { pub id: String, #[cfg(feature = "business_profile_routing")] @@ -154,14 +156,14 @@ impl EuclidAnalysable for ConnectorSelection { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct ConnectorVolumeSplit { pub connector: RoutableConnectorChoice, pub split: u8, } #[cfg(feature = "connector_choice_bcompat")] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] pub enum RoutableChoiceKind { OnlyConnector, FullStruct, @@ -181,15 +183,18 @@ pub enum RoutableChoiceSerde { }, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] #[cfg_attr( feature = "connector_choice_bcompat", serde(from = "RoutableChoiceSerde"), serde(into = "RoutableChoiceSerde") )] #[cfg_attr(not(feature = "connector_choice_bcompat"), derive(PartialEq, Eq))] + +/// Routable Connector chosen for a payment pub struct RoutableConnectorChoice { #[cfg(feature = "connector_choice_bcompat")] + #[serde(skip)] pub choice_kind: RoutableChoiceKind, pub connector: RoutableConnectors, #[cfg(feature = "connector_choice_mca_id")] @@ -287,71 +292,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::Bambora => euclid_enums::Connector::Bambora, - RoutableConnectors::Bankofamerica => euclid_enums::Connector::Bankofamerica, - RoutableConnectors::Bitpay => euclid_enums::Connector::Bitpay, - 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::Prophetpay => euclid_enums::Connector::Prophetpay, - 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, } @@ -387,7 +328,7 @@ impl DetailedConnectorChoice { } } -#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, strum::Display)] +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, strum::Display, ToSchema)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoutingAlgorithmKind { @@ -404,17 +345,19 @@ pub struct RoutingPayloadWrapper { pub profile_id: String, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] #[serde( tag = "type", content = "data", rename_all = "snake_case", try_from = "RoutingAlgorithmSerde" )] +/// Routing Algorithm kind pub enum RoutingAlgorithm { Single(Box), Priority(Vec), VolumeSplit(Vec), + #[schema(value_type=ProgramConnectorSelection)] Advanced(euclid::frontend::ast::Program), } @@ -455,7 +398,7 @@ impl TryFrom for RoutingAlgorithm { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] #[serde( tag = "type", content = "data", @@ -464,8 +407,11 @@ impl TryFrom for RoutingAlgorithm { into = "StraightThroughAlgorithmSerde" )] pub enum StraightThroughAlgorithm { + #[schema(title = "Single")] Single(Box), + #[schema(title = "Priority")] Priority(Vec), + #[schema(title = "VolumeSplit")] VolumeSplit(Vec), } @@ -581,7 +527,7 @@ impl RoutingAlgorithmRef { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct RoutingDictionaryRecord { pub id: String, @@ -594,14 +540,14 @@ pub struct RoutingDictionaryRecord { pub modified_at: i64, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct RoutingDictionary { pub merchant_id: String, pub active_id: Option, pub records: Vec, } -#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Debug, ToSchema)] #[serde(untagged)] pub enum RoutingKind { Config(RoutingDictionary), 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..0777bde85de0 --- /dev/null +++ b/crates/api_models/src/surcharge_decision_configs.rs @@ -0,0 +1,76 @@ +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 SurchargeDetailsOutput { + pub surcharge: SurchargeOutput, + pub tax_on_surcharge: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum SurchargeOutput { + Fixed { amount: 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::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..89f42f58c397 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -1,14 +1,31 @@ -use common_utils::pii; +use common_utils::{crypto::OptionalEncryptableName, pii}; use masking::Secret; +use crate::user_role::UserStatus; +pub mod dashboard_metadata; +#[cfg(feature = "dummy_connector")] +pub mod sample_data; + #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] -pub struct ConnectAccountRequest { +pub struct SignUpWithMerchantIdRequest { + pub name: Secret, pub email: pii::Email, pub password: Secret, + pub company_name: String, } +pub type SignUpWithMerchantIdResponse = AuthorizeResponse; + +#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] +pub struct SignUpRequest { + pub email: pii::Email, + pub password: Secret, +} + +pub type SignUpResponse = DashboardEntryResponse; + #[derive(serde::Serialize, Debug, Clone)] -pub struct ConnectAccountResponse { +pub struct DashboardEntryResponse { pub token: Secret, pub merchant_id: String, pub name: Secret, @@ -19,3 +36,145 @@ pub struct ConnectAccountResponse { #[serde(skip_serializing)] pub user_id: String, } + +pub type SignInRequest = SignUpRequest; + +#[derive(Debug, serde::Serialize)] +#[serde(tag = "flow_type", rename_all = "snake_case")] +pub enum SignInResponse { + MerchantSelect(MerchantSelectResponse), + DashboardEntry(DashboardEntryResponse), +} + +#[derive(Debug, serde::Serialize)] +pub struct MerchantSelectResponse { + pub token: Secret, + pub name: Secret, + pub email: pii::Email, + pub verification_days_left: Option, + pub merchants: Vec, +} + +#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] +pub struct ConnectAccountRequest { + pub email: pii::Email, +} + +pub type ConnectAccountResponse = AuthorizeResponse; + +#[derive(serde::Serialize, Debug, Clone)] +pub struct AuthorizeResponse { + pub is_email_sent: bool, + //this field is added for audit/debug reasons + #[serde(skip_serializing)] + pub user_id: String, + //this field is added for audit/debug reasons + #[serde(skip_serializing)] + pub merchant_id: String, +} + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ChangePasswordRequest { + pub new_password: Secret, + pub old_password: Secret, +} + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ForgotPasswordRequest { + pub email: pii::Email, +} + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ResetPasswordRequest { + pub token: Secret, + pub password: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct InviteUserRequest { + pub email: pii::Email, + pub name: Secret, + pub role_id: String, +} + +#[derive(Debug, serde::Serialize)] +pub struct InviteUserResponse { + pub is_email_sent: bool, + pub password: Option>, +} + +#[derive(Debug, serde::Serialize)] +pub struct InviteMultipleUserResponse { + pub email: pii::Email, + pub is_email_sent: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SwitchMerchantIdRequest { + pub merchant_id: String, +} + +pub type SwitchMerchantResponse = DashboardEntryResponse; + +#[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, +} + +#[derive(Debug, serde::Serialize)] +pub struct GetUsersResponse(pub Vec); + +#[derive(Debug, serde::Serialize)] +pub struct UserDetails { + pub user_id: String, + pub email: pii::Email, + pub name: Secret, + pub role_id: String, + pub role_name: String, + pub status: UserStatus, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub last_modified_at: time::PrimitiveDateTime, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct VerifyEmailRequest { + pub token: Secret, +} + +pub type VerifyEmailResponse = SignInResponse; + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct SendVerifyEmailRequest { + pub email: pii::Email, +} + +#[derive(Debug, serde::Serialize)] +pub struct UserMerchantAccount { + pub merchant_id: String, + pub merchant_name: OptionalEncryptableName, + pub is_active: bool, +} + +#[cfg(feature = "recon")] +#[derive(serde::Serialize, Debug)] +pub struct VerifyTokenResponse { + pub merchant_id: String, + pub user_email: pii::Email, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UpdateUserAccountDetailsRequest { + pub name: Option>, + pub preferred_merchant_id: Option, +} 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..11588bbfbafe --- /dev/null +++ b/crates/api_models/src/user/dashboard_metadata.rs @@ -0,0 +1,149 @@ +use common_enums::CountryAlpha2; +use common_utils::pii; +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), + ConfigurationType(ConfigurationType), + IntegrationCompleted, + SPRoutingConfigured(ConfiguredRouting), + Feedback(Feedback), + ProdIntent(ProdIntent), + 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, Clone)] +pub struct IntegrationMethod { + pub integration_type: String, +} +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub enum ConfigurationType { + Single, + Multiple, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct Feedback { + pub email: pii::Email, + pub description: Option, + pub rating: Option, + pub category: Option, +} +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct ProdIntent { + pub legal_business_name: Option, + pub business_label: Option, + pub business_location: Option, + pub display_name: Option, + pub poc_email: Option, + pub business_type: Option, + pub business_identifier: Option, + pub business_website: Option, + pub poc_name: Option, + pub poc_contact: Option, + pub comments: Option, + pub is_completed: bool, +} + +#[derive(Debug, serde::Deserialize, EnumString, serde::Serialize)] +pub enum GetMetaDataRequest { + ProductionAgreement, + SetupProcessor, + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected, + SecondProcessorConnected, + ConfiguredRouting, + TestPayment, + IntegrationMethod, + ConfigurationType, + IntegrationCompleted, + StripeConnected, + PaypalConnected, + SPRoutingConfigured, + Feedback, + ProdIntent, + 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), + ConfigurationType(Option), + IntegrationCompleted(bool), + StripeConnected(Option), + PaypalConnected(Option), + SPRoutingConfigured(Option), + Feedback(Option), + ProdIntent(Option), + SPTestPayment(bool), + DownloadWoocom(bool), + ConfigureWoocom(bool), + SetupWoocomWebhook(bool), + IsMultipleConfiguration(bool), +} diff --git a/crates/api_models/src/user/sample_data.rs b/crates/api_models/src/user/sample_data.rs new file mode 100644 index 000000000000..6d20b20f369c --- /dev/null +++ b/crates/api_models/src/user/sample_data.rs @@ -0,0 +1,23 @@ +use common_enums::{AuthenticationType, CountryAlpha2}; +use common_utils::{self}; +use time::PrimitiveDateTime; + +use crate::enums::Connector; + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct SampleDataRequest { + pub record: Option, + pub connector: Option>, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub start_time: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub end_time: Option, + // The amount for each sample will be between min_amount and max_amount (in dollars) + pub min_amount: Option, + pub max_amount: Option, + pub currency: Option>, + pub auth_type: Option>, + pub business_country: Option, + pub business_label: Option, + pub profile_id: Option, +} diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs new file mode 100644 index 000000000000..e8c9b777c7f1 --- /dev/null +++ b/crates/api_models/src/user_role.rs @@ -0,0 +1,110 @@ +use common_utils::pii; + +use crate::user::DashboardEntryResponse; + +#[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, + CustomerRead, + CustomerWrite, + FileRead, + FileWrite, + Analytics, + ThreeDsDecisionManagerWrite, + ThreeDsDecisionManagerRead, + SurchargeDecisionManagerWrite, + SurchargeDecisionManagerRead, + UsersRead, + UsersWrite, + MerchantAccountCreate, +} + +#[derive(Debug, serde::Serialize)] +pub enum PermissionModule { + Payments, + Refunds, + MerchantAccount, + Forex, + Connectors, + Routing, + Analytics, + Mandates, + Customer, + Disputes, + Files, + ThreeDsDecisionManager, + SurchargeDecisionManager, + AccountCreate, +} + +#[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, +} + +#[derive(Debug, serde::Serialize)] +pub enum UserStatus { + Active, + InvitationSent, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct AcceptInvitationRequest { + pub merchant_ids: Vec, + pub need_dashboard_entry_response: Option, +} + +pub type AcceptInvitationResponse = DashboardEntryResponse; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct DeleteUserRoleRequest { + pub email: pii::Email, +} 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/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index bc8e75f6d479..37aaac42e0df 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -8,11 +8,18 @@ use crate::{disputes, enums as api_enums, mandates, payments, refunds}; #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Copy)] #[serde(rename_all = "snake_case")] pub enum IncomingWebhookEvent { + /// Authorization + Capture success PaymentIntentFailure, + /// Authorization + Capture failure PaymentIntentSuccess, PaymentIntentProcessing, PaymentIntentPartiallyFunded, PaymentIntentCancelled, + PaymentIntentCancelFailure, + PaymentIntentAuthorizationSuccess, + PaymentIntentAuthorizationFailure, + PaymentIntentCaptureSuccess, + PaymentIntentCaptureFailure, PaymentActionRequired, EventNotSupported, SourceChargeable, @@ -86,7 +93,12 @@ impl From for WebhookFlow { | IncomingWebhookEvent::PaymentIntentProcessing | IncomingWebhookEvent::PaymentActionRequired | IncomingWebhookEvent::PaymentIntentPartiallyFunded - | IncomingWebhookEvent::PaymentIntentCancelled => Self::Payment, + | IncomingWebhookEvent::PaymentIntentCancelled + | IncomingWebhookEvent::PaymentIntentCancelFailure + | IncomingWebhookEvent::PaymentIntentAuthorizationSuccess + | IncomingWebhookEvent::PaymentIntentAuthorizationFailure + | IncomingWebhookEvent::PaymentIntentCaptureSuccess + | IncomingWebhookEvent::PaymentIntentCaptureFailure => Self::Payment, IncomingWebhookEvent::EventNotSupported => Self::ReturnResponse, IncomingWebhookEvent::RefundSuccess | IncomingWebhookEvent::RefundFailure => { Self::Refund @@ -157,13 +169,13 @@ pub struct OutgoingWebhook { #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(tag = "type", content = "object", rename_all = "snake_case")] pub enum OutgoingWebhookContent { - #[schema(value_type = PaymentsResponse)] + #[schema(value_type = PaymentsResponse, title = "PaymentsResponse")] PaymentDetails(payments::PaymentsResponse), - #[schema(value_type = RefundResponse)] + #[schema(value_type = RefundResponse, title = "RefundResponse")] RefundDetails(refunds::RefundResponse), - #[schema(value_type = DisputeResponse)] + #[schema(value_type = DisputeResponse, title = "DisputeResponse")] DisputeDetails(Box), - #[schema(value_type = MandateResponse)] + #[schema(value_type = MandateResponse, title = "MandateResponse")] MandateDetails(Box), } diff --git a/crates/cards/Cargo.toml b/crates/cards/Cargo.toml index 00445936e3d2..6892350ce6f4 100644 --- a/crates/cards/Cargo.toml +++ b/crates/cards/Cargo.toml @@ -12,7 +12,7 @@ license.workspace = true [dependencies] error-stack = "0.3.1" luhn = "1.0.1" -serde = { version = "1.0.163", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.40" time = "0.3.21" @@ -20,5 +20,8 @@ time = "0.3.21" common_utils = { version = "0.1.0", path = "../common_utils" } masking = { version = "0.1.0", path = "../masking" } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } + [dev-dependencies] -serde_json = "1.0.96" +serde_json = "1.0.108" diff --git a/crates/cards/src/validate.rs b/crates/cards/src/validate.rs index db6957057ecc..0bb07b83dc68 100644 --- a/crates/cards/src/validate.rs +++ b/crates/cards/src/validate.rs @@ -1,6 +1,8 @@ use std::{fmt, ops::Deref, str::FromStr}; use masking::{PeekInterface, Strategy, StrongSecret, WithType}; +#[cfg(not(target_arch = "wasm32"))] +use router_env::logger; use serde::{Deserialize, Deserializer, Serialize}; use thiserror::Error; @@ -22,6 +24,13 @@ impl CardNumber { pub fn get_card_isin(self) -> String { self.0.peek().chars().take(6).collect::() } + + pub fn get_extended_card_bin(self) -> String { + self.0.peek().chars().take(8).collect::() + } + pub fn get_card_no(self) -> String { + self.0.peek().chars().collect::() + } pub fn get_last4(self) -> String { self.0 .peek() @@ -33,6 +42,9 @@ impl CardNumber { .rev() .collect::() } + pub fn get_card_extended_bin(self) -> String { + self.0.peek().chars().take(8).collect::() + } } impl FromStr for CardNumber { @@ -72,7 +84,7 @@ impl<'de> Deserialize<'de> for CardNumber { } } -pub struct CardNumberStrategy; +pub enum CardNumberStrategy {} impl Strategy for CardNumberStrategy where @@ -85,7 +97,13 @@ where return WithType::fmt(val, f); } - write!(f, "{}{}", &val_str[..6], "*".repeat(val_str.len() - 6)) + if let Some(value) = val_str.get(..6) { + write!(f, "{}{}", value, "*".repeat(val_str.len() - 6)) + } else { + #[cfg(not(target_arch = "wasm32"))] + logger::error!("Invalid card number {val_str}"); + WithType::fmt(val, f) + } } } diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index 88628825ca64..f82d8e7a825b 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -7,10 +7,14 @@ rust-version.workspace = true readme = "README.md" license.workspace = true +[features] +dummy_connector = [] +openapi = [] + [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } -serde = { version = "1.0.160", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" strum = { version = "0.25", features = ["derive"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } @@ -18,4 +22,4 @@ utoipa = { version = "3.3.0", features = ["preserve_order"] } router_derive = { version = "0.1.0", path = "../router_derive" } [dev-dependencies] -serde_json = "1.0.96" +serde_json = "1.0.108" diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 48b0664c16d3..7330c0708d58 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1,18 +1,19 @@ use std::num::{ParseFloatError, TryFromIntError}; -use router_derive; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[doc(hidden)] 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, DbEventType as EventType, - DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, - DbMandateStatus as MandateStatus, DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, - DbPaymentType as PaymentType, DbRefundStatus as RefundStatus, + DbBlocklistDataKind as BlocklistDataKind, DbCaptureMethod as CaptureMethod, + DbCaptureStatus as CaptureStatus, DbConnectorType as ConnectorType, + DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage, + DbDisputeStatus as DisputeStatus, DbEventType as EventType, DbFutureUsage as FutureUsage, + DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, + DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentType as PaymentType, + DbRefundStatus as RefundStatus, + DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, }; } @@ -29,7 +30,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 +51,7 @@ pub enum AttemptStatus { VoidFailed, AutoRefunded, PartialCharged, + PartialChargedAndChargeable, Unresolved, #[default] Pending, @@ -59,6 +61,110 @@ pub enum AttemptStatus { DeviceDataCollectionPending, } +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + strum::EnumIter, + strum::EnumVariantNames, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +/// Connectors eligible for payments routing +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, + Placetopay, + Powertranz, + Prophetpay, + Rapyd, + Riskified, + Shift4, + Signifyd, + Square, + Stax, + Stripe, + Trustpay, + // Tsys, + Tsys, + Volt, + Wise, + Worldline, + Worldpay, + Zen, +} + impl AttemptStatus { pub fn is_terminal_status(self) -> bool { match self { @@ -68,7 +174,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 +186,7 @@ impl AttemptStatus { | Self::CodInitiated | Self::VoidInitiated | Self::CaptureInitiated - | Self::PartialCharged + | Self::PartialChargedAndChargeable | Self::Unresolved | Self::Pending | Self::PaymentMethodAwaited @@ -105,7 +212,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 +237,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 { @@ -145,6 +252,53 @@ pub enum CaptureStatus { Failed, } +#[derive( + Default, + Clone, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, + Hash, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum AuthorizationStatus { + Success, + Failure, + // Processing state is before calling connector + #[default] + Processing, + // Requires merchant action + Unresolved, +} + +#[derive( + Clone, + Debug, + PartialEq, + Eq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, + Hash, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BlocklistDataKind { + PaymentMethod, + CardBin, + ExtendedCardBin, +} + #[derive( Clone, Copy, @@ -161,7 +315,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 { @@ -176,6 +330,7 @@ pub enum CaptureMethod { Scheduled, } +/// Type of the Connector for the financial use case. Could range from Payments to Accounting to Banking. #[derive( Clone, Copy, @@ -188,7 +343,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 { @@ -212,6 +367,7 @@ pub enum ConnectorType { PaymentMethodAuth, } +/// The three letter ISO currency code in uppercase. Eg: 'USD' for the United States Dollar. #[allow(clippy::upper_case_acronyms)] #[derive( Clone, @@ -229,18 +385,21 @@ 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, AMD, ANG, + AOA, ARS, AUD, AWG, AZN, + BAM, BBD, BDT, + BGN, BHD, BIF, BMD, @@ -249,6 +408,7 @@ pub enum Currency { BRL, BSD, BWP, + BYN, BZD, CAD, CHF, @@ -257,6 +417,7 @@ pub enum Currency { COP, CRC, CUP, + CVE, CZK, DJF, DKK, @@ -266,7 +427,9 @@ pub enum Currency { ETB, EUR, FJD, + FKP, GBP, + GEL, GHS, GIP, GMD, @@ -281,6 +444,7 @@ pub enum Currency { IDR, ILS, INR, + IQD, JMD, JOD, JPY, @@ -297,6 +461,7 @@ pub enum Currency { LKR, LRD, LSL, + LYD, MAD, MDL, MGA, @@ -304,11 +469,13 @@ pub enum Currency { MMK, MNT, MOP, + MRU, MUR, MVR, MWK, MXN, MYR, + MZN, NAD, NGN, NIO, @@ -316,6 +483,7 @@ pub enum Currency { NPR, NZD, OMR, + PAB, PEN, PGK, PHP, @@ -324,34 +492,47 @@ pub enum Currency { PYG, QAR, RON, + RSD, RUB, RWF, SAR, + SBD, SCR, SEK, SGD, + SHP, + SLE, SLL, SOS, + SRD, SSP, + STN, SVC, SZL, THB, + TND, + TOP, TRY, TTD, TWD, TZS, + UAH, UGX, #[default] USD, UYU, UZS, + VES, VND, VUV, + WST, XAF, + XCD, XOF, XPF, YER, ZAR, + ZMW, } impl Currency { @@ -408,12 +589,15 @@ impl Currency { Self::ALL => "008", Self::AMD => "051", Self::ANG => "532", + Self::AOA => "973", Self::ARS => "032", Self::AUD => "036", Self::AWG => "533", Self::AZN => "944", + Self::BAM => "977", Self::BBD => "052", Self::BDT => "050", + Self::BGN => "975", Self::BHD => "048", Self::BIF => "108", Self::BMD => "060", @@ -422,6 +606,7 @@ impl Currency { Self::BRL => "986", Self::BSD => "044", Self::BWP => "072", + Self::BYN => "933", Self::BZD => "084", Self::CAD => "124", Self::CHF => "756", @@ -429,6 +614,7 @@ impl Currency { Self::COP => "170", Self::CRC => "188", Self::CUP => "192", + Self::CVE => "132", Self::CZK => "203", Self::DJF => "262", Self::DKK => "208", @@ -438,7 +624,9 @@ impl Currency { Self::ETB => "230", Self::EUR => "978", Self::FJD => "242", + Self::FKP => "238", Self::GBP => "826", + Self::GEL => "981", Self::GHS => "936", Self::GIP => "292", Self::GMD => "270", @@ -453,6 +641,7 @@ impl Currency { Self::IDR => "360", Self::ILS => "376", Self::INR => "356", + Self::IQD => "368", Self::JMD => "388", Self::JOD => "400", Self::JPY => "392", @@ -469,6 +658,7 @@ impl Currency { Self::LKR => "144", Self::LRD => "430", Self::LSL => "426", + Self::LYD => "434", Self::MAD => "504", Self::MDL => "498", Self::MGA => "969", @@ -476,11 +666,13 @@ impl Currency { Self::MMK => "104", Self::MNT => "496", Self::MOP => "446", + Self::MRU => "929", Self::MUR => "480", Self::MVR => "462", Self::MWK => "454", Self::MXN => "484", Self::MYR => "458", + Self::MZN => "943", Self::NAD => "516", Self::NGN => "566", Self::NIO => "558", @@ -488,6 +680,7 @@ impl Currency { Self::NPR => "524", Self::NZD => "554", Self::OMR => "512", + Self::PAB => "590", Self::PEN => "604", Self::PGK => "598", Self::PHP => "608", @@ -497,33 +690,46 @@ impl Currency { Self::QAR => "634", Self::RON => "946", Self::CNY => "156", + Self::RSD => "941", Self::RUB => "643", Self::RWF => "646", Self::SAR => "682", + Self::SBD => "090", Self::SCR => "690", Self::SEK => "752", Self::SGD => "702", + Self::SHP => "654", + Self::SLE => "925", Self::SLL => "694", Self::SOS => "706", + Self::SRD => "968", Self::SSP => "728", + Self::STN => "930", Self::SVC => "222", Self::SZL => "748", Self::THB => "764", + Self::TND => "788", + Self::TOP => "776", Self::TRY => "949", Self::TTD => "780", Self::TWD => "901", Self::TZS => "834", + Self::UAH => "980", Self::UGX => "800", Self::USD => "840", Self::UYU => "858", Self::UZS => "860", + Self::VES => "928", Self::VND => "704", Self::VUV => "548", + Self::WST => "882", Self::XAF => "950", + Self::XCD => "951", Self::XOF => "952", Self::XPF => "953", Self::YER => "886", Self::ZAR => "710", + Self::ZMW => "967", } } @@ -549,12 +755,15 @@ impl Currency { | Self::ALL | Self::AMD | Self::ANG + | Self::AOA | Self::ARS | Self::AUD | Self::AWG | Self::AZN + | Self::BAM | Self::BBD | Self::BDT + | Self::BGN | Self::BHD | Self::BMD | Self::BND @@ -562,6 +771,7 @@ impl Currency { | Self::BRL | Self::BSD | Self::BWP + | Self::BYN | Self::BZD | Self::CAD | Self::CHF @@ -569,6 +779,7 @@ impl Currency { | Self::COP | Self::CRC | Self::CUP + | Self::CVE | Self::CZK | Self::DKK | Self::DOP @@ -577,7 +788,9 @@ impl Currency { | Self::ETB | Self::EUR | Self::FJD + | Self::FKP | Self::GBP + | Self::GEL | Self::GHS | Self::GIP | Self::GMD @@ -591,6 +804,7 @@ impl Currency { | Self::IDR | Self::ILS | Self::INR + | Self::IQD | Self::JMD | Self::JOD | Self::KES @@ -604,17 +818,20 @@ impl Currency { | Self::LKR | Self::LRD | Self::LSL + | Self::LYD | Self::MAD | Self::MDL | Self::MKD | Self::MMK | Self::MNT | Self::MOP + | Self::MRU | Self::MUR | Self::MVR | Self::MWK | Self::MXN | Self::MYR + | Self::MZN | Self::NAD | Self::NGN | Self::NIO @@ -622,6 +839,7 @@ impl Currency { | Self::NPR | Self::NZD | Self::OMR + | Self::PAB | Self::PEN | Self::PGK | Self::PHP @@ -629,42 +847,60 @@ impl Currency { | Self::PLN | Self::QAR | Self::RON + | Self::RSD | Self::RUB | Self::SAR + | Self::SBD | Self::SCR | Self::SEK | Self::SGD + | Self::SHP + | Self::SLE | Self::SLL | Self::SOS + | Self::SRD | Self::SSP + | Self::STN | Self::SVC | Self::SZL | Self::THB + | Self::TND + | Self::TOP | Self::TRY | Self::TTD | Self::TWD | Self::TZS + | Self::UAH | Self::USD | Self::UYU | Self::UZS + | Self::VES + | Self::WST + | Self::XCD | Self::YER - | Self::ZAR => false, + | Self::ZAR + | Self::ZMW => false, } } pub fn is_three_decimal_currency(self) -> bool { match self { - Self::BHD | Self::JOD | Self::KWD | Self::OMR => true, + Self::BHD | Self::IQD | Self::JOD | Self::KWD | Self::LYD | Self::OMR | Self::TND => { + true + } Self::AED | Self::ALL | Self::AMD + | Self::AOA | Self::ANG | Self::ARS | Self::AUD | Self::AWG | Self::AZN + | Self::BAM | Self::BBD | Self::BDT + | Self::BGN | Self::BIF | Self::BMD | Self::BND @@ -672,6 +908,7 @@ impl Currency { | Self::BRL | Self::BSD | Self::BWP + | Self::BYN | Self::BZD | Self::CAD | Self::CHF @@ -680,6 +917,7 @@ impl Currency { | Self::COP | Self::CRC | Self::CUP + | Self::CVE | Self::CZK | Self::DJF | Self::DKK @@ -689,7 +927,9 @@ impl Currency { | Self::ETB | Self::EUR | Self::FJD + | Self::FKP | Self::GBP + | Self::GEL | Self::GHS | Self::GIP | Self::GMD @@ -725,17 +965,20 @@ impl Currency { | Self::MMK | Self::MNT | Self::MOP + | Self::MRU | Self::MUR | Self::MVR | Self::MWK | Self::MXN | Self::MYR + | Self::MZN | Self::NAD | Self::NGN | Self::NIO | Self::NOK | Self::NPR | Self::NZD + | Self::PAB | Self::PEN | Self::PGK | Self::PHP @@ -744,33 +987,45 @@ impl Currency { | Self::PYG | Self::QAR | Self::RON + | Self::RSD | Self::RUB | Self::RWF | Self::SAR + | Self::SBD | Self::SCR | Self::SEK | Self::SGD + | Self::SHP + | Self::SLE | Self::SLL | Self::SOS + | Self::SRD | Self::SSP + | Self::STN | Self::SVC | Self::SZL | Self::THB + | Self::TOP | Self::TRY | Self::TTD | Self::TWD | Self::TZS + | Self::UAH | Self::UGX | Self::USD | Self::UYU | Self::UZS + | Self::VES | Self::VND | Self::VUV + | Self::WST | Self::XAF + | Self::XCD | Self::XPF | Self::XOF | Self::YER - | Self::ZAR => false, + | Self::ZAR + | Self::ZMW => false, } } } @@ -787,14 +1042,18 @@ 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 { + /// Authorize + Capture success PaymentSucceeded, + /// Authorize + Capture failed PaymentFailed, PaymentProcessing, PaymentCancelled, + PaymentAuthorized, + PaymentCaptured, ActionRequired, RefundSucceeded, RefundFailed, @@ -823,7 +1082,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 +1105,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 +1120,7 @@ pub enum IntentStatus { RequiresConfirmation, RequiresCapture, PartiallyCaptured, + PartiallyCapturedAndCapturable, } #[derive( @@ -879,7 +1139,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 +1161,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 { @@ -917,6 +1177,7 @@ pub enum PaymentMethodIssuerCode { JpBacs, } +/// To indicate the type of payment experience that the customer would go through #[derive( Eq, strum::EnumString, @@ -952,6 +1213,7 @@ pub enum PaymentExperience { DisplayWaitScreen, } +/// Indicates the sub type of payment method. Eg: 'google_pay' & 'apple_pay' for wallets. #[derive( Clone, Copy, @@ -1057,6 +1319,7 @@ pub enum PaymentMethodType { PayEasy, } +/// Indicates the type of payment method. Eg: 'card', 'wallet', etc. #[derive( Clone, Copy, @@ -1092,6 +1355,7 @@ pub enum PaymentMethod { GiftCard, } +/// To be used to specify the type of payment. Use 'setup_mandate' in case of zero auth flow. #[derive( Clone, Copy, @@ -1105,7 +1369,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 { @@ -1129,7 +1393,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, @@ -1140,7 +1404,7 @@ pub enum RefundStatus { TransactionFailure, } -/// The status of the mandate, which indicates whether it can be used to initiate a payment +/// The status of the mandate, which indicates whether it can be used to initiate a payment. #[derive( Clone, Copy, @@ -1154,7 +1418,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 { @@ -1165,6 +1429,7 @@ pub enum MandateStatus { Revoked, } +/// Indicates the card network. #[derive( Clone, Debug, @@ -1208,7 +1473,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 { @@ -1232,7 +1497,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 { @@ -1262,7 +1527,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, @@ -1286,6 +1551,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 { @@ -1689,7 +1977,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 { @@ -1717,7 +2005,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 { @@ -1772,7 +2060,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 { @@ -1781,6 +2069,7 @@ pub enum PaymentSource { Postman, Dashboard, Sdk, + Webhook, } #[derive( @@ -1839,7 +2128,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 { @@ -1854,3 +2143,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_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 3619c93d772c..3e6ee40f3a7b 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -30,14 +30,14 @@ regex = "1.8.4" reqwest = { version = "0.11.18", features = ["json", "native-tls", "gzip", "multipart"] } ring = { version = "0.16.20", features = ["std"] } rustc-hash = "1.1.0" -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" serde_urlencoded = "0.7.1" signal-hook = { version = "0.3.15", optional = true } strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"], optional = true } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"], optional = true } # First party crates common_enums = { version = "0.1.0", path = "../common_enums" } diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 60756192d66e..1e28d2c47f3c 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -24,26 +24,34 @@ 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; /// Header Key for application overhead of a request pub const X_HS_LATENCY: &str = "x-hs-latency"; -/// SDK Default Theme const -pub const DEFAULT_SDK_THEME: &str = "#7EA8F6"; - /// Default Payment Link Background color -pub const DEFAULT_BACKGROUND_COLOR: &str = "#E5E5E5"; +pub const DEFAULT_BACKGROUND_COLOR: &str = "#212E46"; /// Default product Img Link -pub const DEFAULT_PRODUCT_IMG: &str = "https://i.imgur.com/On3VtKF.png"; +pub const DEFAULT_PRODUCT_IMG: &str = + "https://live.hyperswitch.io/payment-link-assets/cart_placeholder.png"; /// Default Merchant Logo Link -pub const DEFAULT_MERCHANT_LOGO: &str = "https://i.imgur.com/RfxPFQo.png"; +pub const DEFAULT_MERCHANT_LOGO: &str = + "https://live.hyperswitch.io/payment-link-assets/Merchant_placeholder.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"; + +/// Default SDK Layout +pub const DEFAULT_SDK_LAYOUT: &str = "tabs"; + +/// Payment intent default client secret expiry (in seconds) +pub const DEFAULT_SESSION_EXPIRY: i64 = 15 * 60; diff --git a/crates/common_utils/src/crypto.rs b/crates/common_utils/src/crypto.rs index a0f365dd40dd..c2c83a3589e4 100644 --- a/crates/common_utils/src/crypto.rs +++ b/crates/common_utils/src/crypto.rs @@ -279,7 +279,10 @@ impl DecodeMessage for GcmAes256 { .change_context(errors::CryptoError::DecodingFailed)?; let nonce_sequence = NonceSequence::from_bytes( - msg[..ring::aead::NONCE_LEN] + msg.get(..ring::aead::NONCE_LEN) + .ok_or(errors::CryptoError::DecodingFailed) + .into_report() + .attach_printable("Failed to read the nonce form the encrypted ciphertext")? .try_into() .into_report() .change_context(errors::CryptoError::DecodingFailed)?, diff --git a/crates/common_utils/src/custom_serde.rs b/crates/common_utils/src/custom_serde.rs index edbfa143a667..e4608c4f3714 100644 --- a/crates/common_utils/src/custom_serde.rs +++ b/crates/common_utils/src/custom_serde.rs @@ -84,6 +84,53 @@ pub mod iso8601 { }) } } + /// Use the well-known ISO 8601 format which is without timezone when serializing and deserializing an + /// [`Option`][PrimitiveDateTime]. + /// + /// [PrimitiveDateTime]: ::time::PrimitiveDateTime + pub mod option_without_timezone { + use serde::{de, Deserialize, Serialize}; + use time::macros::format_description; + + use super::*; + + /// Serialize an [`Option`] using the well-known ISO 8601 format which is without timezone. + pub fn serialize( + date_time: &Option, + serializer: S, + ) -> Result + where + S: Serializer, + { + date_time + .map(|date_time| { + let format = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"); + date_time.assume_utc().format(format) + }) + .transpose() + .map_err(S::Error::custom)? + .serialize(serializer) + } + + /// Deserialize an [`Option`] from its ISO 8601 representation. + pub fn deserialize<'a, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'a>, + { + Option::deserialize(deserializer)? + .map(|time_string| { + let format = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"); + PrimitiveDateTime::parse(time_string, format).map_err(|_| { + de::Error::custom(format!( + "Failed to parse PrimitiveDateTime from {time_string}" + )) + }) + }) + .transpose() + } + } } /// Use the UNIX timestamp when serializing and deserializing an diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 14b8d4de1c36..e755e0f9c4c6 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -40,11 +40,19 @@ pub enum ApiEventsType { }, Routing, ResourceListAPI, - PaymentRedirectionResponse, + PaymentRedirectionResponse { + connector: Option, + payment_id: Option, + }, Gsm, // TODO: This has to be removed once the corresponding apiEventTypes are created Miscellaneous, RustLocker, + FraudCheck, + Recon, + Dispute { + dispute_id: String, + }, } 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/lib.rs b/crates/common_utils/src/lib.rs index 62428dccfb6a..0ac8e886bc06 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -10,6 +10,7 @@ pub mod errors; pub mod events; pub mod ext_traits; pub mod fp_utils; +pub mod macros; pub mod pii; #[allow(missing_docs)] // Todo: add docs pub mod request; diff --git a/crates/common_utils/src/macros.rs b/crates/common_utils/src/macros.rs new file mode 100644 index 000000000000..c07b2112db2c --- /dev/null +++ b/crates/common_utils/src/macros.rs @@ -0,0 +1,104 @@ +#![allow(missing_docs)] + +#[macro_export] +macro_rules! newtype_impl { + ($is_pub:vis, $name:ident, $ty_path:path) => { + impl std::ops::Deref for $name { + type Target = $ty_path; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl std::ops::DerefMut for $name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + impl From<$ty_path> for $name { + fn from(ty: $ty_path) -> Self { + Self(ty) + } + } + + impl $name { + pub fn into_inner(self) -> $ty_path { + self.0 + } + } + }; +} + +#[macro_export] +macro_rules! newtype { + ($is_pub:vis $name:ident = $ty_path:path) => { + $is_pub struct $name(pub $ty_path); + + $crate::newtype_impl!($is_pub, $name, $ty_path); + }; + + ($is_pub:vis $name:ident = $ty_path:path, derives = ($($trt:path),*)) => { + #[derive($($trt),*)] + $is_pub struct $name(pub $ty_path); + + $crate::newtype_impl!($is_pub, $name, $ty_path); + }; +} + +#[macro_export] +macro_rules! async_spawn { + ($t:block) => { + tokio::spawn(async move { $t }); + }; +} + +/// Use this to ensure that the corresponding +/// openapi route has been implemented in the openapi crate +#[macro_export] +macro_rules! openapi_route { + ($route_name: ident) => {{ + #[cfg(feature = "openapi")] + use openapi::routes::$route_name as _; + + $route_name + }}; +} + +#[macro_export] +macro_rules! fallback_reverse_lookup_not_found { + ($a:expr,$b:expr) => { + match $a { + Ok(res) => res, + Err(err) => { + router_env::logger::error!(reverse_lookup_fallback = %err); + match err.current_context() { + errors::StorageError::ValueNotFound(_) => return $b, + errors::StorageError::DatabaseError(data_err) => { + match data_err.current_context() { + diesel_models::errors::DatabaseError::NotFound => return $b, + _ => return Err(err) + } + } + _=> return Err(err) + } + } + }; + }; +} + +#[macro_export] +macro_rules! collect_missing_value_keys { + [$(($key:literal, $option:expr)),+] => { + { + let mut keys: Vec<&'static str> = Vec::new(); + $( + if $option.is_none() { + keys.push($key); + } + )* + keys + } + }; +} diff --git a/crates/common_utils/src/pii.rs b/crates/common_utils/src/pii.rs index c246d2042269..066962af1d46 100644 --- a/crates/common_utils/src/pii.rs +++ b/crates/common_utils/src/pii.rs @@ -12,6 +12,8 @@ use diesel::{ }; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, Secret, Strategy, WithType}; +#[cfg(feature = "logs")] +use router_env::logger; use crate::{ crypto::Encryptable, @@ -27,7 +29,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)] @@ -41,13 +43,14 @@ where fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result { let val_str: &str = val.as_ref(); - // masks everything but the last 4 digits - write!( - f, - "{}{}", - "*".repeat(val_str.len() - 4), - &val_str[val_str.len() - 4..] - ) + if let Some(val_str) = val_str.get(val_str.len() - 4..) { + // masks everything but the last 4 digits + write!(f, "{}{}", "*".repeat(val_str.len() - 4), val_str) + } else { + #[cfg(feature = "logs")] + logger::error!("Invalid phone number: {val_str}"); + WithType::fmt(val, f) + } } } @@ -144,7 +147,7 @@ where /// Strategy for Encryption #[derive(Debug)] -pub struct EncryptionStratergy; +pub enum EncryptionStratergy {} impl Strategy for EncryptionStratergy where @@ -157,7 +160,7 @@ where /// Client secret #[derive(Debug)] -pub struct ClientSecret; +pub enum ClientSecret {} impl Strategy for ClientSecret where @@ -174,22 +177,32 @@ where { return WithType::fmt(val, f); } - write!( - f, - "{}_{}_{}", - client_secret_segments[0], - client_secret_segments[1], - "*".repeat( - val_str.len() - - (client_secret_segments[0].len() + client_secret_segments[1].len() + 2) + + if let Some((client_secret_segments_0, client_secret_segments_1)) = client_secret_segments + .first() + .zip(client_secret_segments.get(1)) + { + write!( + f, + "{}_{}_{}", + client_secret_segments_0, + client_secret_segments_1, + "*".repeat( + val_str.len() + - (client_secret_segments_0.len() + client_secret_segments_1.len() + 2) + ) ) - ) + } else { + #[cfg(feature = "logs")] + logger::error!("Invalid client secret: {val_str}"); + WithType::fmt(val, f) + } } } /// Strategy for masking Email #[derive(Debug)] -pub struct EmailStrategy; +pub enum EmailStrategy {} impl Strategy for EmailStrategy where @@ -231,12 +244,6 @@ impl TryFrom for Email { } } -impl From> for Email { - fn from(value: Secret) -> Self { - Self(value) - } -} - impl ops::Deref for Email { type Target = Secret; @@ -305,7 +312,7 @@ impl FromStr for Email { /// IP address #[derive(Debug)] -pub struct IpAddress; +pub enum IpAddress {} impl Strategy for IpAddress where @@ -325,14 +332,20 @@ where } } - write!(f, "{}.**.**.**", segments[0]) + if let Some(segments) = segments.first() { + write!(f, "{}.**.**.**", segments) + } else { + #[cfg(feature = "logs")] + logger::error!("Invalid IP address: {val_str}"); + WithType::fmt(val, f) + } } } /// 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/request.rs b/crates/common_utils/src/request.rs index 64bce8649d97..f063a1940bef 100644 --- a/crates/common_utils/src/request.rs +++ b/crates/common_utils/src/request.rs @@ -17,6 +17,7 @@ pub enum Method { Post, Put, Delete, + Patch, } #[derive(Deserialize, Serialize, Debug)] @@ -24,6 +25,7 @@ pub enum ContentType { Json, FormUrlEncoded, FormData, + Xml, } fn default_request_headers() -> [(String, Maskable); 1] { @@ -36,12 +38,28 @@ fn default_request_headers() -> [(String, Maskable); 1] { pub struct Request { pub url: String, pub headers: Headers, - pub payload: Option>, pub method: Method, - pub content_type: Option, pub certificate: Option, pub certificate_key: Option, - pub form_data: Option, + pub body: Option, +} + +impl std::fmt::Debug for RequestContent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Json(_) => "JsonRequestBody", + Self::FormUrlEncoded(_) => "FormUrlEncodedRequestBody", + Self::FormData(_) => "FormDataRequestBody", + Self::Xml(_) => "XmlRequestBody", + }) + } +} + +pub enum RequestContent { + Json(Box), + FormUrlEncoded(Box), + FormData(reqwest::multipart::Form), + Xml(Box), } impl Request { @@ -50,16 +68,14 @@ impl Request { method, url: String::from(url), headers: std::collections::HashSet::new(), - payload: None, - content_type: None, certificate: None, certificate_key: None, - form_data: None, + body: None, } } - pub fn set_body(&mut self, body: String) { - self.payload = Some(body.into()); + pub fn set_body>(&mut self, body: T) { + self.body.replace(body.into()); } pub fn add_default_headers(&mut self) { @@ -70,10 +86,6 @@ impl Request { self.headers.insert((String::from(header), value)); } - pub fn add_content_type(&mut self, content_type: ContentType) { - self.content_type = Some(content_type); - } - pub fn add_certificate(&mut self, certificate: Option) { self.certificate = certificate; } @@ -81,22 +93,16 @@ impl Request { pub fn add_certificate_key(&mut self, certificate_key: Option) { self.certificate = certificate_key; } - - pub fn set_form_data(&mut self, form_data: reqwest::multipart::Form) { - self.form_data = Some(form_data); - } } #[derive(Debug)] pub struct RequestBuilder { pub url: String, pub headers: Headers, - pub payload: Option>, pub method: Method, - pub content_type: Option, pub certificate: Option, pub certificate_key: Option, - pub form_data: Option, + pub body: Option, } impl RequestBuilder { @@ -105,11 +111,9 @@ impl RequestBuilder { method: Method::Get, url: String::with_capacity(1024), headers: std::collections::HashSet::new(), - payload: None, - content_type: None, certificate: None, certificate_key: None, - form_data: None, + body: None, } } @@ -134,23 +138,12 @@ impl RequestBuilder { } pub fn headers(mut self, headers: Vec<(String, Maskable)>) -> Self { - let mut h = headers.into_iter().map(|(h, v)| (h, v)); - self.headers.extend(&mut h); + self.headers.extend(headers); self } - pub fn form_data(mut self, form_data: Option) -> Self { - self.form_data = form_data; - self - } - - pub fn body(mut self, option_body: Option) -> Self { - self.payload = option_body.map(RequestBody::get_inner_value); - self - } - - pub fn content_type(mut self, content_type: ContentType) -> Self { - self.content_type = Some(content_type); + pub fn set_body>(mut self, body: T) -> Self { + self.body.replace(body.into()); self } @@ -169,11 +162,9 @@ impl RequestBuilder { method: self.method, url: self.url, headers: self.headers, - payload: self.payload, - content_type: self.content_type, certificate: self.certificate, certificate_key: self.certificate_key, - form_data: self.form_data, + body: self.body, } } } @@ -200,7 +191,15 @@ impl RequestBody { logger::info!(connector_request_body=?body); Ok(Self(Secret::new(encoder(body)?))) } - pub fn get_inner_value(request_body: Self) -> Secret { - request_body.0 + + pub fn get_inner_value(request_body: RequestContent) -> Secret { + match request_body { + RequestContent::Json(i) => serde_json::to_string(&i).unwrap_or_default().into(), + RequestContent::FormUrlEncoded(i) => { + serde_urlencoded::to_string(&i).unwrap_or_default().into() + } + RequestContent::Xml(i) => quick_xml::se::to_string(&i).unwrap_or_default().into(), + RequestContent::FormData(_) => String::new().into(), + } } } diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 111f0f43c0f2..cf94f2fe26ce 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -2,7 +2,10 @@ use error_stack::{IntoReport, ResultExt}; use serde::{de::Visitor, Deserialize, Deserializer}; -use crate::errors::{CustomResult, PercentageError}; +use crate::{ + consts, + errors::{CustomResult, PercentageError}, +}; /// Represents Percentage Value between 0 and 100 both inclusive #[derive(Clone, Default, Debug, PartialEq, serde::Serialize)] @@ -136,3 +139,13 @@ impl<'de, const PRECISION: u8> Deserialize<'de> for Percentage { data.deserialize_map(PercentageVisitor:: {}) } } + +/// represents surcharge type and value +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum Surcharge { + /// Fixed Surcharge value + Fixed(i64), + /// Surcharge percentage + Rate(Percentage<{ consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH }>), +} diff --git a/crates/config_importer/Cargo.toml b/crates/config_importer/Cargo.toml new file mode 100644 index 000000000000..d831812cdcab --- /dev/null +++ b/crates/config_importer/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "config_importer" +description = "Utility to convert a TOML configuration file to a list of environment variables" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +readme = "README.md" +license.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +anyhow = "1.0.75" +clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } +indexmap = { version = "2.1.0", optional = true } +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" +toml = { version = "0.7.4", default-features = false, features = ["parse"] } + +[features] +default = ["preserve_order"] +preserve_order = ["dep:indexmap", "serde_json/preserve_order", "toml/preserve_order"] diff --git a/crates/config_importer/README.md b/crates/config_importer/README.md new file mode 100644 index 000000000000..a7190e6441b7 --- /dev/null +++ b/crates/config_importer/README.md @@ -0,0 +1,41 @@ +# config_importer + +A simple utility tool to import a Hyperswitch TOML configuration file, convert +it into environment variable key-value pairs, and export it in the specified +format. +As of now, it supports only exporting the environment variables to a JSON format +compatible with Kubernetes, but it can be easily extended to export to a YAML +format compatible with Kubernetes or the env file format. + +## Usage + +You can find the usage information from the help message by specifying the +`--help` flag: + +```shell +cargo run --bin config_importer -- --help +``` + +### Specifying the output location + +If the `--output-file` flag is not specified, the utility prints the output to +stdout. +If you would like to write the output to a file instead, you can specify the +`--output-file` flag with the path to the output file: + +```shell +cargo run --bin config_importer -- --input-file config/development.toml --output-file config/development.json +``` + +### Specifying a different prefix + +If the `--prefix` flag is not specified, the default prefix `ROUTER` is +considered, which generates the environment variables accepted by the `router` +binary/application. +If you'd want to generate environment variables for the `drainer` +binary/application, then you can specify the `--prefix` flag with value +`drainer` (or `DRAINER`, both work). + +```shell +cargo run --bin config_importer -- --input-file config/drainer.toml --prefix drainer +``` diff --git a/crates/config_importer/src/cli.rs b/crates/config_importer/src/cli.rs new file mode 100644 index 000000000000..d394f51b4b3f --- /dev/null +++ b/crates/config_importer/src/cli.rs @@ -0,0 +1,43 @@ +use std::path::PathBuf; + +/// Utility to import a hyperswitch TOML configuration file, convert it into environment variable +/// key-value pairs, and export it in the specified format. +#[derive(clap::Parser, Debug)] +#[command(arg_required_else_help = true)] +pub(crate) struct Args { + /// Input TOML configuration file. + #[arg(short, long, value_name = "FILE")] + pub(crate) input_file: PathBuf, + + /// The format to convert the environment variables to. + #[arg( + value_enum, + short = 'f', + long, + value_name = "FORMAT", + default_value = "kubernetes-json" + )] + pub(crate) output_format: OutputFormat, + + /// Output file. Output will be written to stdout if not specified. + #[arg(short, long, value_name = "FILE")] + pub(crate) output_file: Option, + + /// Prefix to be used for each environment variable in the generated output. + #[arg(short, long, default_value = "ROUTER")] + pub(crate) prefix: String, +} + +/// The output format to convert environment variables to. +#[derive(clap::ValueEnum, Clone, Copy, Debug)] +pub(crate) enum OutputFormat { + /// Converts each environment variable to an object containing `name` and `value` fields. + /// + /// ```json + /// { + /// "name": "ENVIRONMENT", + /// "value": "PRODUCTION" + /// } + /// ``` + KubernetesJson, +} diff --git a/crates/config_importer/src/main.rs b/crates/config_importer/src/main.rs new file mode 100644 index 000000000000..2a29ad56dc7e --- /dev/null +++ b/crates/config_importer/src/main.rs @@ -0,0 +1,98 @@ +mod cli; + +use std::io::{BufWriter, Write}; + +use anyhow::Context; + +/// The separator used in environment variable names. +const ENV_VAR_SEPARATOR: &str = "__"; + +#[cfg(not(feature = "preserve_order"))] +type EnvironmentVariableMap = std::collections::HashMap; + +#[cfg(feature = "preserve_order")] +type EnvironmentVariableMap = indexmap::IndexMap; + +fn main() -> anyhow::Result<()> { + let args = ::parse(); + + // Read input TOML file + let toml_contents = + std::fs::read_to_string(args.input_file).context("Failed to read input file")?; + let table = toml_contents + .parse::() + .context("Failed to parse TOML file contents")?; + + // Parse TOML file contents to a `HashMap` of environment variable name and value pairs + let env_vars = table + .iter() + .flat_map(|(key, value)| process_toml_value(&args.prefix, key, value)) + .collect::(); + + let writer: BufWriter> = match args.output_file { + // Write to file if output file is specified + Some(file) => BufWriter::new(Box::new( + std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(file) + .context("Failed to open output file")?, + )), + // Write to stdout otherwise + None => BufWriter::new(Box::new(std::io::stdout().lock())), + }; + + // Write environment variables in specified format + match args.output_format { + cli::OutputFormat::KubernetesJson => { + let k8s_env_vars = env_vars + .into_iter() + .map(|(name, value)| KubernetesEnvironmentVariable { name, value }) + .collect::>(); + serde_json::to_writer_pretty(writer, &k8s_env_vars) + .context("Failed to serialize environment variables as JSON")? + } + } + + Ok(()) +} + +fn process_toml_value( + prefix: impl std::fmt::Display + Clone, + key: impl std::fmt::Display + Clone, + value: &toml::Value, +) -> Vec<(String, String)> { + let key_with_prefix = format!("{prefix}{ENV_VAR_SEPARATOR}{key}").to_ascii_uppercase(); + + match value { + toml::Value::String(s) => vec![(key_with_prefix, s.to_owned())], + toml::Value::Integer(i) => vec![(key_with_prefix, i.to_string())], + toml::Value::Float(f) => vec![(key_with_prefix, f.to_string())], + toml::Value::Boolean(b) => vec![(key_with_prefix, b.to_string())], + toml::Value::Datetime(dt) => vec![(key_with_prefix, dt.to_string())], + toml::Value::Array(values) => { + if values.is_empty() { + return vec![(key_with_prefix, String::new())]; + } + + // This logic does not support / account for arrays of tables or arrays of arrays. + let (_processed_keys, processed_values) = values + .iter() + .flat_map(|v| process_toml_value(prefix.clone(), key.clone(), v)) + .unzip::<_, _, Vec, Vec>(); + vec![(key_with_prefix, processed_values.join(","))] + } + toml::Value::Table(map) => map + .into_iter() + .flat_map(|(k, v)| process_toml_value(key_with_prefix.clone(), k, v)) + .collect(), + } +} + +/// The Kubernetes environment variable structure containing a name and a value. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub(crate) struct KubernetesEnvironmentVariable { + name: String, + value: String, +} diff --git a/crates/connector_configs/Cargo.toml b/crates/connector_configs/Cargo.toml new file mode 100644 index 000000000000..083a741ef50f --- /dev/null +++ b/crates/connector_configs/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "connector_configs" +description = "Connector Integration Dashboard" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[features] +default = ["payouts", "dummy_connector"] +production = [] +development = [] +sandbox = [] +dummy_connector = ["api_models/dummy_connector", "development"] +payouts = [] + +[dependencies] +api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +serde = { version = "1.0.193", features = ["derive"] } +serde_with = "3.4.0" +toml = "0.7.3" +utoipa = { version = "3.3.0", features = ["preserve_order"] } \ No newline at end of file diff --git a/crates/connector_configs/src/common_config.rs b/crates/connector_configs/src/common_config.rs new file mode 100644 index 000000000000..c3cbaab4ab33 --- /dev/null +++ b/crates/connector_configs/src/common_config.rs @@ -0,0 +1,176 @@ +use api_models::{payment_methods, payments}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub struct ZenApplePay { + pub terminal_uuid: Option, + pub pay_wall_secret: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(untagged)] +pub enum ApplePayData { + ApplePay(payments::ApplePayMetadata), + ApplePayCombined(payments::ApplePayCombinedMetadata), + Zen(ZenApplePay), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GpayDashboardPayLoad { + #[serde(skip_serializing_if = "Option::is_none")] + pub gateway_merchant_id: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "stripe:version")] + pub stripe_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename( + serialize = "stripe_publishable_key", + deserialize = "stripe:publishable_key" + ))] + #[serde(alias = "stripe:publishable_key")] + #[serde(alias = "stripe_publishable_key")] + pub stripe_publishable_key: Option, + pub merchant_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub merchant_id: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub struct ZenGooglePay { + pub terminal_uuid: Option, + pub pay_wall_secret: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(untagged)] +pub enum GooglePayData { + Standard(GpayDashboardPayLoad), + Zen(ZenGooglePay), +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(untagged)] +pub enum GoogleApiModelData { + Standard(payments::GpayMetaData), + Zen(ZenGooglePay), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct PaymentMethodsEnabled { + pub payment_method: api_models::enums::PaymentMethod, + pub payment_method_types: Option>, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct ApiModelMetaData { + pub merchant_config_currency: Option, + pub merchant_account_id: Option, + pub account_name: Option, + pub terminal_id: Option, + pub merchant_id: Option, + pub google_pay: Option, + pub apple_pay: Option, + pub apple_pay_combined: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct CardProvider { + pub payment_method_type: api_models::enums::CardNetwork, + /// List of currencies accepted or has the processing capabilities of the processor + #[schema(example = json!( + { + "type": "specific_accepted", + "list": ["USD", "INR"] + } + ), value_type = Option)] + pub accepted_currencies: Option, + #[schema(example = json!( + { + "type": "specific_accepted", + "list": ["UK", "AU"] + } + ), value_type = Option)] + pub accepted_countries: Option, +} +#[serde_with::skip_serializing_none] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct Provider { + pub payment_method_type: api_models::enums::PaymentMethodType, + /// List of currencies accepted or has the processing capabilities of the processor + #[schema(example = json!( + { + "type": "specific_accepted", + "list": ["USD", "INR"] + } + ), value_type = Option)] + pub accepted_currencies: Option, + #[schema(example = json!( + { + "type": "specific_accepted", + "list": ["UK", "AU"] + } + ), value_type = Option)] + pub accepted_countries: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct ConnectorApiIntegrationPayload { + pub connector_type: String, + pub profile_id: String, + pub connector_name: api_models::enums::Connector, + #[serde(skip_deserializing)] + #[schema(example = "stripe_US_travel")] + pub connector_label: Option, + pub merchant_connector_id: Option, + pub disabled: bool, + pub test_mode: bool, + pub payment_methods_enabled: Option>, + pub metadata: Option, + pub connector_webhook_details: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct DashboardPaymentMethodPayload { + pub payment_method: api_models::enums::PaymentMethod, + pub payment_method_type: String, + pub provider: Option>, + pub card_provider: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct DashboardRequestPayload { + pub connector: api_models::enums::Connector, + pub payment_methods_enabled: Option>, + pub metadata: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub struct DashboardMetaData { + pub merchant_config_currency: Option, + pub merchant_account_id: Option, + pub account_name: Option, + pub terminal_id: Option, + pub merchant_id: Option, + pub google_pay: Option, + pub apple_pay: Option, + pub apple_pay_combined: Option, +} diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs new file mode 100644 index 000000000000..f0997d53107e --- /dev/null +++ b/crates/connector_configs/src/connector.rs @@ -0,0 +1,274 @@ +use std::collections::HashMap; + +#[cfg(feature = "payouts")] +use api_models::enums::PayoutConnectors; +use api_models::{enums::Connector, payments}; +use serde::Deserialize; +#[cfg(any(feature = "sandbox", feature = "development", feature = "production"))] +use toml; + +use crate::common_config::{CardProvider, GooglePayData, Provider, ZenApplePay}; + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Classic { + pub password_classic: String, + pub username_classic: String, + pub merchant_id_classic: String, +} + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Evoucher { + pub password_evoucher: String, + pub username_evoucher: String, + pub merchant_id_evoucher: String, +} + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CurrencyAuthKeyType { + pub classic: Classic, + pub evoucher: Evoucher, +} + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum ConnectorAuthType { + HeaderKey { + api_key: String, + }, + BodyKey { + api_key: String, + key1: String, + }, + SignatureKey { + api_key: String, + key1: String, + api_secret: String, + }, + MultiAuthKey { + api_key: String, + key1: String, + api_secret: String, + key2: String, + }, + CurrencyAuthKey { + auth_key_map: HashMap, + }, + #[default] + NoKey, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(untagged)] +pub enum ApplePayTomlConfig { + Standard(payments::ApplePayMetadata), + Zen(ZenApplePay), +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +pub struct ConfigMetadata { + pub merchant_config_currency: Option, + pub merchant_account_id: Option, + pub account_name: Option, + pub terminal_id: Option, + pub google_pay: Option, + pub apple_pay: Option, + pub merchant_id: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +pub struct ConnectorTomlConfig { + pub connector_auth: Option, + pub connector_webhook_details: Option, + pub metadata: Option, + pub credit: Option>, + pub debit: Option>, + pub bank_transfer: Option>, + pub bank_redirect: Option>, + pub bank_debit: Option>, + pub pay_later: Option>, + pub wallet: Option>, + pub crypto: Option>, + pub reward: Option>, + pub upi: Option>, + pub voucher: Option>, + pub gift_card: Option>, + pub card_redirect: Option>, + pub is_verifiable: Option, +} +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +pub struct ConnectorConfig { + pub aci: Option, + pub adyen: Option, + #[cfg(feature = "payouts")] + pub adyen_payout: Option, + pub airwallex: Option, + pub authorizedotnet: Option, + pub bankofamerica: Option, + pub bitpay: Option, + pub bluesnap: Option, + pub boku: Option, + pub braintree: Option, + pub cashtocode: Option, + pub checkout: Option, + pub coinbase: Option, + pub cryptopay: Option, + pub cybersource: Option, + pub iatapay: Option, + pub opennode: Option, + pub bambora: Option, + pub dlocal: Option, + pub fiserv: Option, + pub forte: Option, + pub globalpay: Option, + pub globepay: Option, + pub gocardless: Option, + pub helcim: Option, + pub klarna: Option, + pub mollie: Option, + pub multisafepay: Option, + pub nexinets: Option, + pub nmi: Option, + pub noon: Option, + pub nuvei: Option, + pub payme: Option, + pub paypal: Option, + pub payu: Option, + pub placetopay: Option, + pub plaid: Option, + pub powertranz: Option, + pub prophetpay: Option, + pub riskified: Option, + pub rapyd: Option, + pub shift4: Option, + pub stripe: Option, + pub signifyd: Option, + pub trustpay: Option, + pub tsys: Option, + pub volt: Option, + #[cfg(feature = "payouts")] + pub wise_payout: Option, + pub worldline: Option, + pub worldpay: Option, + pub zen: Option, + pub square: Option, + pub stax: Option, + pub dummy_connector: Option, + pub stripe_test: Option, + pub paypal_test: Option, +} + +impl ConnectorConfig { + fn new() -> Result { + #[cfg(all( + feature = "production", + not(any(feature = "sandbox", feature = "development")) + ))] + let config = toml::from_str::(include_str!("../toml/production.toml")); + #[cfg(all( + feature = "sandbox", + not(any(feature = "production", feature = "development")) + ))] + let config = toml::from_str::(include_str!("../toml/sandbox.toml")); + #[cfg(feature = "development")] + let config = toml::from_str::(include_str!("../toml/development.toml")); + + #[cfg(not(any(feature = "sandbox", feature = "development", feature = "production")))] + return Err(String::from( + "Atleast one features has to be enabled for connectorconfig", + )); + + #[cfg(any(feature = "sandbox", feature = "development", feature = "production"))] + match config { + Ok(data) => Ok(data), + Err(err) => Err(err.to_string()), + } + } + + #[cfg(feature = "payouts")] + pub fn get_payout_connector_config( + connector: PayoutConnectors, + ) -> Result, String> { + let connector_data = Self::new()?; + match connector { + PayoutConnectors::Adyen => Ok(connector_data.adyen_payout), + PayoutConnectors::Wise => Ok(connector_data.wise_payout), + } + } + + pub fn get_connector_config( + connector: Connector, + ) -> Result, String> { + let connector_data = Self::new()?; + match connector { + Connector::Aci => Ok(connector_data.aci), + Connector::Adyen => Ok(connector_data.adyen), + Connector::Airwallex => Ok(connector_data.airwallex), + Connector::Authorizedotnet => Ok(connector_data.authorizedotnet), + Connector::Bankofamerica => Ok(connector_data.bankofamerica), + Connector::Bitpay => Ok(connector_data.bitpay), + Connector::Bluesnap => Ok(connector_data.bluesnap), + Connector::Boku => Ok(connector_data.boku), + Connector::Braintree => Ok(connector_data.braintree), + Connector::Cashtocode => Ok(connector_data.cashtocode), + Connector::Checkout => Ok(connector_data.checkout), + Connector::Coinbase => Ok(connector_data.coinbase), + Connector::Cryptopay => Ok(connector_data.cryptopay), + Connector::Cybersource => Ok(connector_data.cybersource), + Connector::Iatapay => Ok(connector_data.iatapay), + Connector::Opennode => Ok(connector_data.opennode), + Connector::Bambora => Ok(connector_data.bambora), + Connector::Dlocal => Ok(connector_data.dlocal), + Connector::Fiserv => Ok(connector_data.fiserv), + Connector::Forte => Ok(connector_data.forte), + Connector::Globalpay => Ok(connector_data.globalpay), + Connector::Globepay => Ok(connector_data.globepay), + Connector::Gocardless => Ok(connector_data.gocardless), + Connector::Helcim => Ok(connector_data.helcim), + Connector::Klarna => Ok(connector_data.klarna), + Connector::Mollie => Ok(connector_data.mollie), + Connector::Multisafepay => Ok(connector_data.multisafepay), + Connector::Nexinets => Ok(connector_data.nexinets), + Connector::Prophetpay => Ok(connector_data.prophetpay), + Connector::Nmi => Ok(connector_data.nmi), + Connector::Noon => Ok(connector_data.noon), + Connector::Nuvei => Ok(connector_data.nuvei), + Connector::Payme => Ok(connector_data.payme), + Connector::Paypal => Ok(connector_data.paypal), + Connector::Payu => Ok(connector_data.payu), + Connector::Placetopay => Ok(connector_data.placetopay), + Connector::Plaid => Ok(connector_data.plaid), + Connector::Powertranz => Ok(connector_data.powertranz), + Connector::Rapyd => Ok(connector_data.rapyd), + Connector::Riskified => Ok(connector_data.riskified), + Connector::Shift4 => Ok(connector_data.shift4), + Connector::Signifyd => Ok(connector_data.signifyd), + Connector::Square => Ok(connector_data.square), + Connector::Stax => Ok(connector_data.stax), + Connector::Stripe => Ok(connector_data.stripe), + Connector::Trustpay => Ok(connector_data.trustpay), + Connector::Tsys => Ok(connector_data.tsys), + Connector::Volt => Ok(connector_data.volt), + Connector::Wise => Err("Use get_payout_connector_config".to_string()), + Connector::Worldline => Ok(connector_data.worldline), + Connector::Worldpay => Ok(connector_data.worldpay), + Connector::Zen => Ok(connector_data.zen), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector1 => Ok(connector_data.dummy_connector), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector2 => Ok(connector_data.dummy_connector), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector3 => Ok(connector_data.dummy_connector), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector4 => Ok(connector_data.stripe_test), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector5 => Ok(connector_data.dummy_connector), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector6 => Ok(connector_data.dummy_connector), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector7 => Ok(connector_data.paypal_test), + } + } +} diff --git a/crates/connector_configs/src/lib.rs b/crates/connector_configs/src/lib.rs new file mode 100644 index 000000000000..d480871c747f --- /dev/null +++ b/crates/connector_configs/src/lib.rs @@ -0,0 +1,4 @@ +pub mod common_config; +pub mod connector; +pub mod response_modifier; +pub mod transformer; diff --git a/crates/connector_configs/src/response_modifier.rs b/crates/connector_configs/src/response_modifier.rs new file mode 100644 index 000000000000..80332612c13a --- /dev/null +++ b/crates/connector_configs/src/response_modifier.rs @@ -0,0 +1,368 @@ +use crate::common_config::{ + CardProvider, ConnectorApiIntegrationPayload, DashboardMetaData, DashboardPaymentMethodPayload, + DashboardRequestPayload, GoogleApiModelData, GooglePayData, GpayDashboardPayLoad, Provider, +}; + +impl ConnectorApiIntegrationPayload { + pub fn get_transformed_response_payload(response: Self) -> DashboardRequestPayload { + let mut wallet_details: Vec = Vec::new(); + let mut bank_redirect_details: Vec = Vec::new(); + let mut pay_later_details: Vec = Vec::new(); + let mut debit_details: Vec = Vec::new(); + let mut credit_details: Vec = Vec::new(); + let mut bank_transfer_details: Vec = Vec::new(); + let mut crypto_details: Vec = Vec::new(); + let mut bank_debit_details: Vec = Vec::new(); + let mut reward_details: Vec = Vec::new(); + let mut upi_details: Vec = Vec::new(); + let mut voucher_details: Vec = Vec::new(); + let mut gift_card_details: Vec = Vec::new(); + let mut card_redirect_details: Vec = Vec::new(); + + if let Some(payment_methods_enabled) = response.payment_methods_enabled.clone() { + for methods in payment_methods_enabled { + match methods.payment_method { + api_models::enums::PaymentMethod::Card => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + match method_type.payment_method_type { + api_models::enums::PaymentMethodType::Credit => { + if let Some(card_networks) = method_type.card_networks { + for card in card_networks { + credit_details.push(CardProvider { + payment_method_type: card, + accepted_currencies: method_type + .accepted_currencies + .clone(), + accepted_countries: method_type + .accepted_countries + .clone(), + }) + } + } + } + api_models::enums::PaymentMethodType::Debit => { + if let Some(card_networks) = method_type.card_networks { + for card in card_networks { + // debit_details.push(card) + debit_details.push(CardProvider { + payment_method_type: card, + accepted_currencies: method_type + .accepted_currencies + .clone(), + accepted_countries: method_type + .accepted_countries + .clone(), + }) + } + } + } + _ => (), + } + } + } + } + api_models::enums::PaymentMethod::Wallet => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + // wallet_details.push(method_type.payment_method_type) + wallet_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + api_models::enums::PaymentMethod::BankRedirect => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + bank_redirect_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + api_models::enums::PaymentMethod::PayLater => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + pay_later_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + api_models::enums::PaymentMethod::BankTransfer => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + bank_transfer_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + api_models::enums::PaymentMethod::Crypto => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + crypto_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + api_models::enums::PaymentMethod::BankDebit => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + bank_debit_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + api_models::enums::PaymentMethod::Reward => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + reward_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + api_models::enums::PaymentMethod::Upi => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + upi_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + api_models::enums::PaymentMethod::Voucher => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + voucher_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + api_models::enums::PaymentMethod::GiftCard => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + gift_card_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + api_models::enums::PaymentMethod::CardRedirect => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + card_redirect_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) + } + } + } + } + } + } + + let upi = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Upi, + payment_method_type: api_models::enums::PaymentMethod::Upi.to_string(), + provider: Some(upi_details), + card_provider: None, + }; + + let voucher: DashboardPaymentMethodPayload = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Voucher, + payment_method_type: api_models::enums::PaymentMethod::Voucher.to_string(), + provider: Some(voucher_details), + card_provider: None, + }; + + let gift_card: DashboardPaymentMethodPayload = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::GiftCard, + payment_method_type: api_models::enums::PaymentMethod::GiftCard.to_string(), + provider: Some(gift_card_details), + card_provider: None, + }; + + let reward = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Reward, + payment_method_type: api_models::enums::PaymentMethod::Reward.to_string(), + provider: Some(reward_details), + card_provider: None, + }; + + let wallet = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Wallet, + payment_method_type: api_models::enums::PaymentMethod::Wallet.to_string(), + provider: Some(wallet_details), + card_provider: None, + }; + let bank_redirect = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::BankRedirect, + payment_method_type: api_models::enums::PaymentMethod::BankRedirect.to_string(), + provider: Some(bank_redirect_details), + card_provider: None, + }; + + let bank_debit = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::BankDebit, + payment_method_type: api_models::enums::PaymentMethod::BankDebit.to_string(), + provider: Some(bank_debit_details), + card_provider: None, + }; + + let bank_transfer = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::BankTransfer, + payment_method_type: api_models::enums::PaymentMethod::BankTransfer.to_string(), + provider: Some(bank_transfer_details), + card_provider: None, + }; + + let crypto = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Crypto, + payment_method_type: api_models::enums::PaymentMethod::Crypto.to_string(), + provider: Some(crypto_details), + card_provider: None, + }; + + let card_redirect = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::CardRedirect, + payment_method_type: api_models::enums::PaymentMethod::CardRedirect.to_string(), + provider: Some(card_redirect_details), + card_provider: None, + }; + let pay_later = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::PayLater, + payment_method_type: api_models::enums::PaymentMethod::PayLater.to_string(), + provider: Some(pay_later_details), + card_provider: None, + }; + let debit_details = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Card, + payment_method_type: api_models::enums::PaymentMethodType::Debit.to_string(), + provider: None, + card_provider: Some(debit_details), + }; + let credit_details = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Card, + payment_method_type: api_models::enums::PaymentMethodType::Credit.to_string(), + provider: None, + card_provider: Some(credit_details), + }; + + let google_pay = Self::get_google_pay_metadata_response(response.clone()); + let account_name = match response.metadata.clone() { + Some(meta_data) => meta_data.account_name, + _ => None, + }; + + let merchant_account_id = match response.metadata.clone() { + Some(meta_data) => meta_data.merchant_account_id, + _ => None, + }; + let merchant_id = match response.metadata.clone() { + Some(meta_data) => meta_data.merchant_id, + _ => None, + }; + let terminal_id = match response.metadata.clone() { + Some(meta_data) => meta_data.terminal_id, + _ => None, + }; + let apple_pay = match response.metadata.clone() { + Some(meta_data) => meta_data.apple_pay, + _ => None, + }; + let apple_pay_combined = match response.metadata.clone() { + Some(meta_data) => meta_data.apple_pay_combined, + _ => None, + }; + let merchant_config_currency = match response.metadata.clone() { + Some(meta_data) => meta_data.merchant_config_currency, + _ => None, + }; + + let meta_data = DashboardMetaData { + merchant_config_currency, + merchant_account_id, + apple_pay, + apple_pay_combined, + google_pay, + account_name, + terminal_id, + merchant_id, + }; + + DashboardRequestPayload { + connector: response.connector_name, + payment_methods_enabled: Some(vec![ + upi, + voucher, + reward, + wallet, + bank_redirect, + bank_debit, + bank_transfer, + crypto, + card_redirect, + pay_later, + debit_details, + credit_details, + gift_card, + ]), + metadata: Some(meta_data), + } + } + + pub fn get_google_pay_metadata_response(response: Self) -> Option { + match response.metadata { + Some(meta_data) => { + match meta_data.google_pay { + Some(google_pay) => match google_pay { + GoogleApiModelData::Standard(standard_data) => { + let data = standard_data.allowed_payment_methods.first().map( + |allowed_pm| { + allowed_pm.tokenization_specification.parameters.clone() + }, + )?; + Some(GooglePayData::Standard(GpayDashboardPayLoad { + gateway_merchant_id: data.gateway_merchant_id, + stripe_version: data.stripe_version, + stripe_publishable_key: data.stripe_publishable_key, + merchant_name: standard_data.merchant_info.merchant_name, + merchant_id: standard_data.merchant_info.merchant_id, + })) + } + GoogleApiModelData::Zen(data) => Some(GooglePayData::Zen(data)), + }, + None => None, + } + } + None => None, + } + } +} diff --git a/crates/connector_configs/src/transformer.rs b/crates/connector_configs/src/transformer.rs new file mode 100644 index 000000000000..be68a0c5f94b --- /dev/null +++ b/crates/connector_configs/src/transformer.rs @@ -0,0 +1,284 @@ +use std::str::FromStr; + +use api_models::{ + enums::{ + Connector, PaymentMethod, PaymentMethodType, + PaymentMethodType::{AliPay, ApplePay, GooglePay, Klarna, Paypal, WeChatPay}, + }, + payment_methods, payments, +}; + +use crate::common_config::{ + ApiModelMetaData, ConnectorApiIntegrationPayload, DashboardMetaData, DashboardRequestPayload, + GoogleApiModelData, GooglePayData, PaymentMethodsEnabled, Provider, +}; + +impl DashboardRequestPayload { + pub fn transform_card( + payment_method_type: PaymentMethodType, + card_provider: Vec, + ) -> payment_methods::RequestPaymentMethodTypes { + payment_methods::RequestPaymentMethodTypes { + payment_method_type, + card_networks: Some(card_provider), + minimum_amount: Some(0), + maximum_amount: Some(68607706), + recurring_enabled: true, + installment_payment_enabled: false, + accepted_currencies: None, + accepted_countries: None, + payment_experience: None, + } + } + + pub fn get_payment_experience( + connector: Connector, + payment_method_type: PaymentMethodType, + payment_method: PaymentMethod, + ) -> Option { + match payment_method { + PaymentMethod::BankRedirect => None, + _ => match (connector, payment_method_type) { + #[cfg(feature = "dummy_connector")] + (Connector::DummyConnector4, _) | (Connector::DummyConnector7, _) => { + Some(api_models::enums::PaymentExperience::RedirectToUrl) + } + (Connector::Zen, GooglePay) | (Connector::Zen, ApplePay) => { + Some(api_models::enums::PaymentExperience::RedirectToUrl) + } + (Connector::Braintree, Paypal) | (Connector::Klarna, Klarna) => { + Some(api_models::enums::PaymentExperience::InvokeSdkClient) + } + (Connector::Globepay, AliPay) + | (Connector::Globepay, WeChatPay) + | (Connector::Stripe, WeChatPay) => { + Some(api_models::enums::PaymentExperience::DisplayQrCode) + } + (_, GooglePay) | (_, ApplePay) => { + Some(api_models::enums::PaymentExperience::InvokeSdkClient) + } + _ => Some(api_models::enums::PaymentExperience::RedirectToUrl), + }, + } + } + pub fn transform_payment_method( + connector: Connector, + provider: Vec, + payment_method: PaymentMethod, + ) -> Vec { + let mut payment_method_types = Vec::new(); + for method_type in provider { + let data = payment_methods::RequestPaymentMethodTypes { + payment_method_type: method_type.payment_method_type, + card_networks: None, + minimum_amount: Some(0), + maximum_amount: Some(68607706), + recurring_enabled: true, + installment_payment_enabled: false, + accepted_currencies: method_type.accepted_currencies, + accepted_countries: method_type.accepted_countries, + payment_experience: Self::get_payment_experience( + connector, + method_type.payment_method_type, + payment_method, + ), + }; + payment_method_types.push(data) + } + payment_method_types + } + + pub fn create_connector_request( + request: Self, + api_response: ConnectorApiIntegrationPayload, + ) -> ConnectorApiIntegrationPayload { + let mut card_payment_method_types = Vec::new(); + let mut payment_method_enabled = Vec::new(); + + if let Some(payment_methods_enabled) = request.payment_methods_enabled.clone() { + for payload in payment_methods_enabled { + match payload.payment_method { + api_models::enums::PaymentMethod::Card => { + if let Some(card_provider) = payload.card_provider { + let payment_type = api_models::enums::PaymentMethodType::from_str( + &payload.payment_method_type, + ) + .map_err(|_| "Invalid key received".to_string()); + + if let Ok(payment_type) = payment_type { + for method in card_provider { + let data = payment_methods::RequestPaymentMethodTypes { + payment_method_type: payment_type, + card_networks: Some(vec![method.payment_method_type]), + minimum_amount: Some(0), + maximum_amount: Some(68607706), + recurring_enabled: true, + installment_payment_enabled: false, + accepted_currencies: None, + accepted_countries: None, + payment_experience: None, + }; + card_payment_method_types.push(data) + } + } + } + } + + api_models::enums::PaymentMethod::Wallet + | api_models::enums::PaymentMethod::BankRedirect + | api_models::enums::PaymentMethod::PayLater + | api_models::enums::PaymentMethod::BankTransfer + | api_models::enums::PaymentMethod::Crypto + | api_models::enums::PaymentMethod::BankDebit + | api_models::enums::PaymentMethod::Reward + | api_models::enums::PaymentMethod::Upi + | api_models::enums::PaymentMethod::Voucher + | api_models::enums::PaymentMethod::GiftCard + | api_models::enums::PaymentMethod::CardRedirect => { + if let Some(provider) = payload.provider { + let val = Self::transform_payment_method( + request.connector, + provider, + payload.payment_method, + ); + if !val.is_empty() { + let methods = PaymentMethodsEnabled { + payment_method: payload.payment_method, + payment_method_types: Some(val), + }; + payment_method_enabled.push(methods); + } + } + } + }; + } + if !card_payment_method_types.is_empty() { + let card = PaymentMethodsEnabled { + payment_method: api_models::enums::PaymentMethod::Card, + payment_method_types: Some(card_payment_method_types), + }; + payment_method_enabled.push(card); + } + } + + let metadata = Self::transform_metedata(request); + ConnectorApiIntegrationPayload { + connector_type: api_response.connector_type, + profile_id: api_response.profile_id, + connector_name: api_response.connector_name, + connector_label: api_response.connector_label, + merchant_connector_id: api_response.merchant_connector_id, + disabled: api_response.disabled, + test_mode: api_response.test_mode, + payment_methods_enabled: Some(payment_method_enabled), + connector_webhook_details: api_response.connector_webhook_details, + metadata, + } + } + + pub fn transform_metedata(request: Self) -> Option { + let default_metadata = DashboardMetaData { + apple_pay_combined: None, + google_pay: None, + apple_pay: None, + account_name: None, + terminal_id: None, + merchant_account_id: None, + merchant_id: None, + merchant_config_currency: None, + }; + let meta_data = match request.metadata { + Some(data) => data, + None => default_metadata, + }; + let google_pay = Self::get_google_pay_details(meta_data.clone(), request.connector); + let account_name = meta_data.account_name.clone(); + let merchant_account_id = meta_data.merchant_account_id.clone(); + let merchant_id = meta_data.merchant_id.clone(); + let terminal_id = meta_data.terminal_id.clone(); + let apple_pay = meta_data.apple_pay; + let apple_pay_combined = meta_data.apple_pay_combined; + let merchant_config_currency = meta_data.merchant_config_currency; + Some(ApiModelMetaData { + google_pay, + apple_pay, + account_name, + merchant_account_id, + terminal_id, + merchant_id, + merchant_config_currency, + apple_pay_combined, + }) + } + + fn get_custom_gateway_name(connector: Connector) -> String { + match connector { + Connector::Checkout => String::from("checkoutltd"), + Connector::Nuvei => String::from("nuveidigital"), + Connector::Authorizedotnet => String::from("authorizenet"), + Connector::Globalpay => String::from("globalpayments"), + Connector::Bankofamerica | Connector::Cybersource => String::from("cybersource"), + _ => connector.to_string(), + } + } + fn get_google_pay_details( + meta_data: DashboardMetaData, + connector: Connector, + ) -> Option { + match meta_data.google_pay { + Some(gpay_data) => { + let google_pay_data = match gpay_data { + GooglePayData::Standard(data) => { + let token_parameter = payments::GpayTokenParameters { + gateway: Self::get_custom_gateway_name(connector), + gateway_merchant_id: data.gateway_merchant_id, + stripe_version: match connector { + Connector::Stripe => Some(String::from("2018-10-31")), + _ => None, + }, + stripe_publishable_key: match connector { + Connector::Stripe => data.stripe_publishable_key, + _ => None, + }, + }; + let merchant_info = payments::GpayMerchantInfo { + merchant_name: data.merchant_name, + merchant_id: data.merchant_id, + }; + let token_specification = payments::GpayTokenizationSpecification { + token_specification_type: String::from("PAYMENT_GATEWAY"), + parameters: token_parameter, + }; + let allowed_payment_methods_parameters = + payments::GpayAllowedMethodsParameters { + allowed_auth_methods: vec![ + "PAN_ONLY".to_string(), + "CRYPTOGRAM_3DS".to_string(), + ], + allowed_card_networks: vec![ + "AMEX".to_string(), + "DISCOVER".to_string(), + "INTERAC".to_string(), + "JCB".to_string(), + "MASTERCARD".to_string(), + "VISA".to_string(), + ], + }; + let allowed_payment_methods = payments::GpayAllowedPaymentMethods { + payment_method_type: String::from("CARD"), + parameters: allowed_payment_methods_parameters, + tokenization_specification: token_specification, + }; + GoogleApiModelData::Standard(payments::GpayMetaData { + merchant_info, + allowed_payment_methods: vec![allowed_payment_methods], + }) + } + GooglePayData::Zen(data) => GoogleApiModelData::Zen(data), + }; + Some(google_pay_data) + } + _ => None, + } + } +} diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml new file mode 100644 index 000000000000..a8188323dce8 --- /dev/null +++ b/crates/connector_configs/toml/development.toml @@ -0,0 +1,2521 @@ +[aci] +[[aci.credit]] + payment_method_type = "Mastercard" +[[aci.credit]] + payment_method_type = "Visa" +[[aci.credit]] + payment_method_type = "Interac" +[[aci.credit]] + payment_method_type = "AmericanExpress" +[[aci.credit]] + payment_method_type = "JCB" +[[aci.credit]] + payment_method_type = "DinersClub" +[[aci.credit]] + payment_method_type = "Discover" +[[aci.credit]] + payment_method_type = "CartesBancaires" +[[aci.credit]] + payment_method_type = "UnionPay" +[[aci.debit]] + payment_method_type = "Mastercard" +[[aci.debit]] + payment_method_type = "Visa" +[[aci.debit]] + payment_method_type = "Interac" +[[aci.debit]] + payment_method_type = "AmericanExpress" +[[aci.debit]] + payment_method_type = "JCB" +[[aci.debit]] + payment_method_type = "DinersClub" +[[aci.debit]] + payment_method_type = "Discover" +[[aci.debit]] + payment_method_type = "CartesBancaires" +[[aci.debit]] + payment_method_type = "UnionPay" +[[aci.wallet]] + payment_method_type = "ali_pay" +[[aci.wallet]] + payment_method_type = "mb_way" +[[aci.bank_redirect]] + payment_method_type = "ideal" +[[aci.bank_redirect]] + payment_method_type = "giropay" +[[aci.bank_redirect]] + payment_method_type = "sofort" +[[aci.bank_redirect]] + payment_method_type = "eps" +[[aci.bank_redirect]] + payment_method_type = "przelewy24" +[[aci.bank_redirect]] + payment_method_type = "trustly" +[[aci.bank_redirect]] + payment_method_type = "interac" +[aci.connector_auth.BodyKey] +api_key="API Key" +key1="Entity ID" +[aci.connector_webhook_details] +merchant_secret="Source verification key" + + +[adyen] +[[adyen.credit]] + payment_method_type = "Mastercard" +[[adyen.credit]] + payment_method_type = "Visa" +[[adyen.credit]] + payment_method_type = "Interac" +[[adyen.credit]] + payment_method_type = "AmericanExpress" +[[adyen.credit]] + payment_method_type = "JCB" +[[adyen.credit]] + payment_method_type = "DinersClub" +[[adyen.credit]] + payment_method_type = "Discover" +[[adyen.credit]] + payment_method_type = "CartesBancaires" +[[adyen.credit]] + payment_method_type = "UnionPay" +[[adyen.debit]] + payment_method_type = "Mastercard" +[[adyen.debit]] + payment_method_type = "Visa" +[[adyen.debit]] + payment_method_type = "Interac" +[[adyen.debit]] + payment_method_type = "AmericanExpress" +[[adyen.debit]] + payment_method_type = "JCB" +[[adyen.debit]] + payment_method_type = "DinersClub" +[[adyen.debit]] + payment_method_type = "Discover" +[[adyen.debit]] + payment_method_type = "CartesBancaires" +[[adyen.debit]] + payment_method_type = "UnionPay" +[[adyen.pay_later]] + payment_method_type = "klarna" +[[adyen.pay_later]] + payment_method_type = "affirm" +[[adyen.pay_later]] + payment_method_type = "afterpay_clearpay" +[[adyen.pay_later]] + payment_method_type = "pay_bright" +[[adyen.pay_later]] + payment_method_type = "walley" +[[adyen.pay_later]] + payment_method_type = "alma" +[[adyen.pay_later]] + payment_method_type = "atome" +[[adyen.bank_debit]] + payment_method_type = "ach" +[[adyen.bank_debit]] + payment_method_type = "bacs" +[[adyen.bank_debit]] + payment_method_type = "sepa" +[[adyen.bank_redirect]] + payment_method_type = "ideal" +[[adyen.bank_redirect]] + payment_method_type = "giropay" +[[adyen.bank_redirect]] + payment_method_type = "sofort" +[[adyen.bank_redirect]] + payment_method_type = "eps" +[[adyen.bank_redirect]] + payment_method_type = "blik" +[[adyen.bank_redirect]] + payment_method_type = "przelewy24" +[[adyen.bank_redirect]] + payment_method_type = "trustly" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_czech_republic" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_finland" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_poland" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_slovakia" +[[adyen.bank_redirect]] + payment_method_type = "bancontact_card" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_fpx" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_thailand" +[[adyen.bank_redirect]] + payment_method_type = "bizum" +[[adyen.bank_redirect]] + payment_method_type = "open_banking_uk" +[[adyen.bank_transfer]] + payment_method_type = "permata_bank_transfer" +[[adyen.bank_transfer]] + payment_method_type = "bca_bank_transfer" +[[adyen.bank_transfer]] + payment_method_type = "bni_va" +[[adyen.bank_transfer]] + payment_method_type = "bri_va" +[[adyen.bank_transfer]] + payment_method_type = "cimb_va" +[[adyen.bank_transfer]] + payment_method_type = "danamon_va" +[[adyen.bank_transfer]] + payment_method_type = "mandiri_va" +[[adyen.bank_transfer]] + payment_method_type = "pix" +[[adyen.wallet]] + payment_method_type = "apple_pay" +[[adyen.wallet]] + payment_method_type = "google_pay" +[[adyen.wallet]] + payment_method_type = "paypal" +[[adyen.wallet]] + payment_method_type = "we_chat_pay" +[[adyen.wallet]] + payment_method_type = "ali_pay" +[[adyen.wallet]] + payment_method_type = "mb_way" +[[adyen.wallet]] + payment_method_type = "ali_pay_hk" +[[adyen.wallet]] + payment_method_type = "go_pay" +[[adyen.wallet]] + payment_method_type = "kakao_pay" +[[adyen.wallet]] + payment_method_type = "twint" +[[adyen.wallet]] + payment_method_type = "gcash" +[[adyen.wallet]] + payment_method_type = "vipps" +[[adyen.wallet]] + payment_method_type = "dana" +[[adyen.wallet]] + payment_method_type = "momo" +[[adyen.wallet]] + payment_method_type = "swish" +[[adyen.wallet]] + payment_method_type = "touch_n_go" +[[adyen.voucher]] + payment_method_type = "boleto" +[[adyen.voucher]] + payment_method_type = "alfamart" +[[adyen.voucher]] + payment_method_type = "indomaret" +[[adyen.voucher]] + payment_method_type = "oxxo" +[[adyen.voucher]] + payment_method_type = "seven_eleven" +[[adyen.voucher]] + payment_method_type = "lawson" +[[adyen.voucher]] + payment_method_type = "mini_stop" +[[adyen.voucher]] + payment_method_type = "family_mart" +[[adyen.voucher]] + payment_method_type = "seicomart" +[[adyen.voucher]] + payment_method_type = "pay_easy" +[[adyen.gift_card]] + payment_method_type = "pay_safe_card" +[[adyen.gift_card]] + payment_method_type = "givex" +[[adyen.card_redirect]] + payment_method_type = "benefit" +[[adyen.card_redirect]] + payment_method_type = "knet" +[[adyen.card_redirect]] + payment_method_type = "momo_atm" +[adyen.connector_auth.BodyKey] +api_key="Adyen API Key" +key1="Adyen Account Id" +[adyen.connector_webhook_details] +merchant_secret="Source verification key" +[adyen.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[adyen.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[adyen.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[airwallex] +[[airwallex.credit]] + payment_method_type = "Mastercard" +[[airwallex.credit]] + payment_method_type = "Visa" +[[airwallex.credit]] + payment_method_type = "Interac" +[[airwallex.credit]] + payment_method_type = "AmericanExpress" +[[airwallex.credit]] + payment_method_type = "JCB" +[[airwallex.credit]] + payment_method_type = "DinersClub" +[[airwallex.credit]] + payment_method_type = "Discover" +[[airwallex.credit]] + payment_method_type = "CartesBancaires" +[[airwallex.credit]] + payment_method_type = "UnionPay" +[[airwallex.debit]] + payment_method_type = "Mastercard" +[[airwallex.debit]] + payment_method_type = "Visa" +[[airwallex.debit]] + payment_method_type = "Interac" +[[airwallex.debit]] + payment_method_type = "AmericanExpress" +[[airwallex.debit]] + payment_method_type = "JCB" +[[airwallex.debit]] + payment_method_type = "DinersClub" +[[airwallex.debit]] + payment_method_type = "Discover" +[[airwallex.debit]] + payment_method_type = "CartesBancaires" +[[airwallex.debit]] + payment_method_type = "UnionPay" +[[airwallex.wallet]] + payment_method_type = "google_pay" +[airwallex.connector_auth.BodyKey] +api_key="API Key" +key1="Client ID" +[airwallex.connector_webhook_details] +merchant_secret="Source verification key" + + +[authorizedotnet] +[[authorizedotnet.credit]] + payment_method_type = "Mastercard" +[[authorizedotnet.credit]] + payment_method_type = "Visa" +[[authorizedotnet.credit]] + payment_method_type = "Interac" +[[authorizedotnet.credit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.credit]] + payment_method_type = "JCB" +[[authorizedotnet.credit]] + payment_method_type = "DinersClub" +[[authorizedotnet.credit]] + payment_method_type = "Discover" +[[authorizedotnet.credit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.credit]] + payment_method_type = "UnionPay" +[[authorizedotnet.debit]] + payment_method_type = "Mastercard" +[[authorizedotnet.debit]] + payment_method_type = "Visa" +[[authorizedotnet.debit]] + payment_method_type = "Interac" +[[authorizedotnet.debit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.debit]] + payment_method_type = "JCB" +[[authorizedotnet.debit]] + payment_method_type = "DinersClub" +[[authorizedotnet.debit]] + payment_method_type = "Discover" +[[authorizedotnet.debit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.debit]] + payment_method_type = "UnionPay" +[[authorizedotnet.wallet]] + payment_method_type = "google_pay" +[[authorizedotnet.wallet]] + payment_method_type = "paypal" +[authorizedotnet.connector_auth.BodyKey] +api_key="API Login ID" +key1="Transaction Key" +[authorizedotnet.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" +[authorizedotnet.connector_webhook_details] +merchant_secret="Source verification key" + +[bambora] +[[bambora.credit]] + payment_method_type = "Mastercard" +[[bambora.credit]] + payment_method_type = "Visa" +[[bambora.credit]] + payment_method_type = "Interac" +[[bambora.credit]] + payment_method_type = "AmericanExpress" +[[bambora.credit]] + payment_method_type = "JCB" +[[bambora.credit]] + payment_method_type = "DinersClub" +[[bambora.credit]] + payment_method_type = "Discover" +[[bambora.credit]] + payment_method_type = "CartesBancaires" +[[bambora.credit]] + payment_method_type = "UnionPay" +[[bambora.debit]] + payment_method_type = "Mastercard" +[[bambora.debit]] + payment_method_type = "Visa" +[[bambora.debit]] + payment_method_type = "Interac" +[[bambora.debit]] + payment_method_type = "AmericanExpress" +[[bambora.debit]] + payment_method_type = "JCB" +[[bambora.debit]] + payment_method_type = "DinersClub" +[[bambora.debit]] + payment_method_type = "Discover" +[[bambora.debit]] + payment_method_type = "CartesBancaires" +[[bambora.debit]] + payment_method_type = "UnionPay" +[[bambora.wallet]] + payment_method_type = "apple_pay" +[[bambora.wallet]] + payment_method_type = "paypal" +[bambora.connector_auth.BodyKey] +api_key="Passcode" +key1="Merchant Id" +[bambora.connector_webhook_details] +merchant_secret="Source verification key" +[bambora.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bambora.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica] +[[bankofamerica.credit]] + payment_method_type = "Mastercard" +[[bankofamerica.credit]] + payment_method_type = "Visa" +[[bankofamerica.credit]] + payment_method_type = "Interac" +[[bankofamerica.credit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.credit]] + payment_method_type = "JCB" +[[bankofamerica.credit]] + payment_method_type = "DinersClub" +[[bankofamerica.credit]] + payment_method_type = "Discover" +[[bankofamerica.credit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.credit]] + payment_method_type = "UnionPay" +[[bankofamerica.debit]] + payment_method_type = "Mastercard" +[[bankofamerica.debit]] + payment_method_type = "Visa" +[[bankofamerica.debit]] + payment_method_type = "Interac" +[[bankofamerica.debit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.debit]] + payment_method_type = "JCB" +[[bankofamerica.debit]] + payment_method_type = "DinersClub" +[[bankofamerica.debit]] + payment_method_type = "Discover" +[[bankofamerica.debit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.debit]] + payment_method_type = "UnionPay" +[[bankofamerica.wallet]] + payment_method_type = "apple_pay" +[[bankofamerica.wallet]] + payment_method_type = "google_pay" +[bankofamerica.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[bankofamerica.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bankofamerica.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[bitpay] +[[bitpay.crypto]] + payment_method_type = "crypto_currency" +[bitpay.connector_auth.HeaderKey] +api_key="API Key" +[bitpay.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap] +[[bluesnap.credit]] + payment_method_type = "Mastercard" +[[bluesnap.credit]] + payment_method_type = "Visa" +[[bluesnap.credit]] + payment_method_type = "Interac" +[[bluesnap.credit]] + payment_method_type = "AmericanExpress" +[[bluesnap.credit]] + payment_method_type = "JCB" +[[bluesnap.credit]] + payment_method_type = "DinersClub" +[[bluesnap.credit]] + payment_method_type = "Discover" +[[bluesnap.credit]] + payment_method_type = "CartesBancaires" +[[bluesnap.credit]] + payment_method_type = "UnionPay" +[[bluesnap.debit]] + payment_method_type = "Mastercard" +[[bluesnap.debit]] + payment_method_type = "Visa" +[[bluesnap.debit]] + payment_method_type = "Interac" +[[bluesnap.debit]] + payment_method_type = "AmericanExpress" +[[bluesnap.debit]] + payment_method_type = "JCB" +[[bluesnap.debit]] + payment_method_type = "DinersClub" +[[bluesnap.debit]] + payment_method_type = "Discover" +[[bluesnap.debit]] + payment_method_type = "CartesBancaires" +[[bluesnap.debit]] + payment_method_type = "UnionPay" +[[bluesnap.wallet]] + payment_method_type = "google_pay" +[[bluesnap.wallet]] + payment_method_type = "apple_pay" +[bluesnap.connector_auth.BodyKey] +api_key="Password" +key1="Username" +[bluesnap.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[bluesnap.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bluesnap.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" +[bluesnap.metadata] +merchant_id="Merchant Id" + +[boku] +[[boku.wallet]] + payment_method_type = "dana" +[[boku.wallet]] + payment_method_type = "gcash" +[[boku.wallet]] + payment_method_type = "go_pay" +[[boku.wallet]] + payment_method_type = "kakao_pay" +[[boku.wallet]] + payment_method_type = "momo" +[boku.connector_auth.BodyKey] +api_key="API KEY" +key1= "MERCHANT ID" +[boku.connector_webhook_details] +merchant_secret="Source verification key" + + +[braintree] +[[braintree.credit]] + payment_method_type = "Mastercard" +[[braintree.credit]] + payment_method_type = "Visa" +[[braintree.credit]] + payment_method_type = "Interac" +[[braintree.credit]] + payment_method_type = "AmericanExpress" +[[braintree.credit]] + payment_method_type = "JCB" +[[braintree.credit]] + payment_method_type = "DinersClub" +[[braintree.credit]] + payment_method_type = "Discover" +[[braintree.credit]] + payment_method_type = "CartesBancaires" +[[braintree.credit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "Mastercard" +[[braintree.debit]] + payment_method_type = "Visa" +[[braintree.debit]] + payment_method_type = "Interac" +[[braintree.debit]] + payment_method_type = "AmericanExpress" +[[braintree.debit]] + payment_method_type = "JCB" +[[braintree.debit]] + payment_method_type = "DinersClub" +[[braintree.debit]] + payment_method_type = "Discover" +[[braintree.debit]] + payment_method_type = "CartesBancaires" +[[braintree.debit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "UnionPay" +[braintree.connector_webhook_details] +merchant_secret="Source verification key" + + +[braintree.connector_auth.SignatureKey] +api_key="Public Key" +key1="Merchant Id" +api_secret="Private Key" +[braintree.metadata] +merchant_account_id="Merchant Account Id" +merchant_config_currency="Currency" + +[cashtocode] +[[cashtocode.reward]] + payment_method_type = "classic" +[[cashtocode.reward]] + payment_method_type = "evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_webhook_details] +merchant_secret="Source verification key" + +[checkout] +[[checkout.credit]] + payment_method_type = "Mastercard" +[[checkout.credit]] + payment_method_type = "Visa" +[[checkout.credit]] + payment_method_type = "Interac" +[[checkout.credit]] + payment_method_type = "AmericanExpress" +[[checkout.credit]] + payment_method_type = "JCB" +[[checkout.credit]] + payment_method_type = "DinersClub" +[[checkout.credit]] + payment_method_type = "Discover" +[[checkout.credit]] + payment_method_type = "CartesBancaires" +[[checkout.credit]] + payment_method_type = "UnionPay" +[[checkout.debit]] + payment_method_type = "Mastercard" +[[checkout.debit]] + payment_method_type = "Visa" +[[checkout.debit]] + payment_method_type = "Interac" +[[checkout.debit]] + payment_method_type = "AmericanExpress" +[[checkout.debit]] + payment_method_type = "JCB" +[[checkout.debit]] + payment_method_type = "DinersClub" +[[checkout.debit]] + payment_method_type = "Discover" +[[checkout.debit]] + payment_method_type = "CartesBancaires" +[[checkout.debit]] + payment_method_type = "UnionPay" +[[checkout.wallet]] + payment_method_type = "apple_pay" +[[checkout.wallet]] + payment_method_type = "google_pay" +[[checkout.wallet]] + payment_method_type = "paypal" +[checkout.connector_auth.SignatureKey] +api_key="Checkout API Public Key" +key1="Processing Channel ID" +api_secret="Checkout API Secret Key" +[checkout.connector_webhook_details] +merchant_secret="Source verification key" + +[checkout.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[checkout.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[checkout.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[coinbase] +[[coinbase.crypto]] + payment_method_type = "crypto_currency" +[coinbase.connector_auth.HeaderKey] +api_key="API Key" +[coinbase.connector_webhook_details] +merchant_secret="Source verification key" + +[cryptopay] +[[cryptopay.crypto]] + payment_method_type = "crypto_currency" +[cryptopay.connector_auth.BodyKey] +api_key="API Key" +key1="Secret Key" +[cryptopay.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource] +[[cybersource.credit]] + payment_method_type = "Mastercard" +[[cybersource.credit]] + payment_method_type = "Visa" +[[cybersource.credit]] + payment_method_type = "Interac" +[[cybersource.credit]] + payment_method_type = "AmericanExpress" +[[cybersource.credit]] + payment_method_type = "JCB" +[[cybersource.credit]] + payment_method_type = "DinersClub" +[[cybersource.credit]] + payment_method_type = "Discover" +[[cybersource.credit]] + payment_method_type = "CartesBancaires" +[[cybersource.credit]] + payment_method_type = "UnionPay" +[[cybersource.debit]] + payment_method_type = "Mastercard" +[[cybersource.debit]] + payment_method_type = "Visa" +[[cybersource.debit]] + payment_method_type = "Interac" +[[cybersource.debit]] + payment_method_type = "AmericanExpress" +[[cybersource.debit]] + payment_method_type = "JCB" +[[cybersource.debit]] + payment_method_type = "DinersClub" +[[cybersource.debit]] + payment_method_type = "Discover" +[[cybersource.debit]] + payment_method_type = "CartesBancaires" +[[cybersource.debit]] + payment_method_type = "UnionPay" +[[cybersource.wallet]] + payment_method_type = "apple_pay" +[[cybersource.wallet]] + payment_method_type = "google_pay" +[cybersource.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[cybersource.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[cybersource.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[cybersource.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[dlocal] +[[dlocal.credit]] + payment_method_type = "Mastercard" +[[dlocal.credit]] + payment_method_type = "Visa" +[[dlocal.credit]] + payment_method_type = "Interac" +[[dlocal.credit]] + payment_method_type = "AmericanExpress" +[[dlocal.credit]] + payment_method_type = "JCB" +[[dlocal.credit]] + payment_method_type = "DinersClub" +[[dlocal.credit]] + payment_method_type = "Discover" +[[dlocal.credit]] + payment_method_type = "CartesBancaires" +[[dlocal.credit]] + payment_method_type = "UnionPay" +[[dlocal.debit]] + payment_method_type = "Mastercard" +[[dlocal.debit]] + payment_method_type = "Visa" +[[dlocal.debit]] + payment_method_type = "Interac" +[[dlocal.debit]] + payment_method_type = "AmericanExpress" +[[dlocal.debit]] + payment_method_type = "JCB" +[[dlocal.debit]] + payment_method_type = "DinersClub" +[[dlocal.debit]] + payment_method_type = "Discover" +[[dlocal.debit]] + payment_method_type = "CartesBancaires" +[[dlocal.debit]] + payment_method_type = "UnionPay" +[dlocal.connector_auth.SignatureKey] +api_key="X Login" +key1="X Trans Key" +api_secret="Secret Key" +[dlocal.connector_webhook_details] +merchant_secret="Source verification key" + +[fiserv] +[[fiserv.credit]] + payment_method_type = "Mastercard" +[[fiserv.credit]] + payment_method_type = "Visa" +[[fiserv.credit]] + payment_method_type = "Interac" +[[fiserv.credit]] + payment_method_type = "AmericanExpress" +[[fiserv.credit]] + payment_method_type = "JCB" +[[fiserv.credit]] + payment_method_type = "DinersClub" +[[fiserv.credit]] + payment_method_type = "Discover" +[[fiserv.credit]] + payment_method_type = "CartesBancaires" +[[fiserv.credit]] + payment_method_type = "UnionPay" +[[fiserv.debit]] + payment_method_type = "Mastercard" +[[fiserv.debit]] + payment_method_type = "Visa" +[[fiserv.debit]] + payment_method_type = "Interac" +[[fiserv.debit]] + payment_method_type = "AmericanExpress" +[[fiserv.debit]] + payment_method_type = "JCB" +[[fiserv.debit]] + payment_method_type = "DinersClub" +[[fiserv.debit]] + payment_method_type = "Discover" +[[fiserv.debit]] + payment_method_type = "CartesBancaires" +[[fiserv.debit]] + payment_method_type = "UnionPay" +[fiserv.connector_auth.SignatureKey] +api_key="API Key" +key1="Merchant ID" +api_secret="API Secret" +[fiserv.metadata] +terminal_id="Terminal ID" +[fiserv.connector_webhook_details] +merchant_secret="Source verification key" + +[forte] +[[forte.credit]] + payment_method_type = "Mastercard" +[[forte.credit]] + payment_method_type = "Visa" +[[forte.credit]] + payment_method_type = "Interac" +[[forte.credit]] + payment_method_type = "AmericanExpress" +[[forte.credit]] + payment_method_type = "JCB" +[[forte.credit]] + payment_method_type = "DinersClub" +[[forte.credit]] + payment_method_type = "Discover" +[[forte.credit]] + payment_method_type = "CartesBancaires" +[[forte.credit]] + payment_method_type = "UnionPay" +[[forte.debit]] + payment_method_type = "Mastercard" +[[forte.debit]] + payment_method_type = "Visa" +[[forte.debit]] + payment_method_type = "Interac" +[[forte.debit]] + payment_method_type = "AmericanExpress" +[[forte.debit]] + payment_method_type = "JCB" +[[forte.debit]] + payment_method_type = "DinersClub" +[[forte.debit]] + payment_method_type = "Discover" +[[forte.debit]] + payment_method_type = "CartesBancaires" +[[forte.debit]] + payment_method_type = "UnionPay" +[forte.connector_auth.MultiAuthKey] +api_key="API Access ID" +key1="Organization ID" +api_secret="API Secure Key" +key2="Location ID" +[forte.connector_webhook_details] +merchant_secret="Source verification key" + +[globalpay] +[[globalpay.credit]] + payment_method_type = "Mastercard" +[[globalpay.credit]] + payment_method_type = "Visa" +[[globalpay.credit]] + payment_method_type = "Interac" +[[globalpay.credit]] + payment_method_type = "AmericanExpress" +[[globalpay.credit]] + payment_method_type = "JCB" +[[globalpay.credit]] + payment_method_type = "DinersClub" +[[globalpay.credit]] + payment_method_type = "Discover" +[[globalpay.credit]] + payment_method_type = "CartesBancaires" +[[globalpay.credit]] + payment_method_type = "UnionPay" +[[globalpay.debit]] + payment_method_type = "Mastercard" +[[globalpay.debit]] + payment_method_type = "Visa" +[[globalpay.debit]] + payment_method_type = "Interac" +[[globalpay.debit]] + payment_method_type = "AmericanExpress" +[[globalpay.debit]] + payment_method_type = "JCB" +[[globalpay.debit]] + payment_method_type = "DinersClub" +[[globalpay.debit]] + payment_method_type = "Discover" +[[globalpay.debit]] + payment_method_type = "CartesBancaires" +[[globalpay.debit]] + payment_method_type = "UnionPay" +[[globalpay.bank_redirect]] + payment_method_type = "ideal" +[[globalpay.bank_redirect]] + payment_method_type = "giropay" +[[globalpay.bank_redirect]] + payment_method_type = "sofort" +[[globalpay.bank_redirect]] + payment_method_type = "eps" +[[globalpay.wallet]] + payment_method_type = "google_pay" +[[globalpay.wallet]] + payment_method_type = "paypal" +[globalpay.connector_auth.BodyKey] +api_key="Global App Key" +key1="Global App ID" +[globalpay.metadata] +account_name="Account Name" +[globalpay.connector_webhook_details] +merchant_secret="Source verification key" +[globalpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[globepay] +[[globepay.wallet]] + payment_method_type = "we_chat_pay" +[[globepay.wallet]] + payment_method_type = "ali_pay" +[globepay.connector_auth.BodyKey] +api_key="Partner Code" +key1="Credential Code" +[globepay.connector_webhook_details] +merchant_secret="Source verification key" + +[gocardless] +[[gocardless.bank_debit]] + payment_method_type = "ach" +[[gocardless.bank_debit]] + payment_method_type = "becs" +[[gocardless.bank_debit]] + payment_method_type = "sepa" +[gocardless.connector_auth.HeaderKey] +api_key="Access Token" +[gocardless.connector_webhook_details] +merchant_secret="Source verification key" + +[iatapay] +[[iatapay.upi]] + payment_method_type = "upi_collect" +[iatapay.connector_auth.SignatureKey] +api_key="Client ID" +key1="Airline ID" +api_secret="Client Secret" +[iatapay.connector_webhook_details] +merchant_secret="Source verification key" + +[klarna] +[[klarna.pay_later]] + payment_method_type = "klarna" +[klarna.connector_auth.HeaderKey] +api_key="Klarna API Key" +[klarna.connector_webhook_details] +merchant_secret="Source verification key" + +[mollie] +[[mollie.credit]] + payment_method_type = "Mastercard" +[[mollie.credit]] + payment_method_type = "Visa" +[[mollie.credit]] + payment_method_type = "Interac" +[[mollie.credit]] + payment_method_type = "AmericanExpress" +[[mollie.credit]] + payment_method_type = "JCB" +[[mollie.credit]] + payment_method_type = "DinersClub" +[[mollie.credit]] + payment_method_type = "Discover" +[[mollie.credit]] + payment_method_type = "CartesBancaires" +[[mollie.credit]] + payment_method_type = "UnionPay" +[[mollie.debit]] + payment_method_type = "Mastercard" +[[mollie.debit]] + payment_method_type = "Visa" +[[mollie.debit]] + payment_method_type = "Interac" +[[mollie.debit]] + payment_method_type = "AmericanExpress" +[[mollie.debit]] + payment_method_type = "JCB" +[[mollie.debit]] + payment_method_type = "DinersClub" +[[mollie.debit]] + payment_method_type = "Discover" +[[mollie.debit]] + payment_method_type = "CartesBancaires" +[[mollie.debit]] + payment_method_type = "UnionPay" +[[mollie.bank_redirect]] + payment_method_type = "ideal" +[[mollie.bank_redirect]] + payment_method_type = "giropay" +[[mollie.bank_redirect]] + payment_method_type = "sofort" +[[mollie.bank_redirect]] + payment_method_type = "eps" +[[mollie.bank_redirect]] + payment_method_type = "przelewy24" +[[mollie.bank_redirect]] + payment_method_type = "bancontact_card" +[[mollie.wallet]] + payment_method_type = "paypal" +[mollie.connector_auth.BodyKey] +api_key="API Key" +key1="Profile Token" +[mollie.connector_webhook_details] +merchant_secret="Source verification key" + +[multisafepay] +[[multisafepay.credit]] + payment_method_type = "Mastercard" +[[multisafepay.credit]] + payment_method_type = "Visa" +[[multisafepay.credit]] + payment_method_type = "Interac" +[[multisafepay.credit]] + payment_method_type = "AmericanExpress" +[[multisafepay.credit]] + payment_method_type = "JCB" +[[multisafepay.credit]] + payment_method_type = "DinersClub" +[[multisafepay.credit]] + payment_method_type = "Discover" +[[multisafepay.credit]] + payment_method_type = "CartesBancaires" +[[multisafepay.credit]] + payment_method_type = "UnionPay" +[[multisafepay.debit]] + payment_method_type = "Mastercard" +[[multisafepay.debit]] + payment_method_type = "Visa" +[[multisafepay.debit]] + payment_method_type = "Interac" +[[multisafepay.debit]] + payment_method_type = "AmericanExpress" +[[multisafepay.debit]] + payment_method_type = "JCB" +[[multisafepay.debit]] + payment_method_type = "DinersClub" +[[multisafepay.debit]] + payment_method_type = "Discover" +[[multisafepay.debit]] + payment_method_type = "CartesBancaires" +[[multisafepay.debit]] + payment_method_type = "UnionPay" +[[multisafepay.wallet]] + payment_method_type = "google_pay" +[[multisafepay.wallet]] + payment_method_type = "paypal" +[multisafepay.connector_auth.HeaderKey] +api_key="Enter API Key" +[multisafepay.connector_webhook_details] +merchant_secret="Source verification key" +[multisafepay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nexinets] +[[nexinets.credit]] + payment_method_type = "Mastercard" +[[nexinets.credit]] + payment_method_type = "Visa" +[[nexinets.credit]] + payment_method_type = "Interac" +[[nexinets.credit]] + payment_method_type = "AmericanExpress" +[[nexinets.credit]] + payment_method_type = "JCB" +[[nexinets.credit]] + payment_method_type = "DinersClub" +[[nexinets.credit]] + payment_method_type = "Discover" +[[nexinets.credit]] + payment_method_type = "CartesBancaires" +[[nexinets.credit]] + payment_method_type = "UnionPay" +[[nexinets.debit]] + payment_method_type = "Mastercard" +[[nexinets.debit]] + payment_method_type = "Visa" +[[nexinets.debit]] + payment_method_type = "Interac" +[[nexinets.debit]] + payment_method_type = "AmericanExpress" +[[nexinets.debit]] + payment_method_type = "JCB" +[[nexinets.debit]] + payment_method_type = "DinersClub" +[[nexinets.debit]] + payment_method_type = "Discover" +[[nexinets.debit]] + payment_method_type = "CartesBancaires" +[[nexinets.debit]] + payment_method_type = "UnionPay" +[[nexinets.bank_redirect]] + payment_method_type = "ideal" +[[nexinets.bank_redirect]] + payment_method_type = "giropay" +[[nexinets.bank_redirect]] + payment_method_type = "sofort" +[[nexinets.bank_redirect]] + payment_method_type = "eps" +[[nexinets.wallet]] + payment_method_type = "apple_pay" +[[nexinets.wallet]] + payment_method_type = "paypal" +[nexinets.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant ID" +[nexinets.connector_webhook_details] +merchant_secret="Source verification key" +[nexinets.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nexinets.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[nmi] +[[nmi.credit]] + payment_method_type = "Mastercard" +[[nmi.credit]] + payment_method_type = "Visa" +[[nmi.credit]] + payment_method_type = "Interac" +[[nmi.credit]] + payment_method_type = "AmericanExpress" +[[nmi.credit]] + payment_method_type = "JCB" +[[nmi.credit]] + payment_method_type = "DinersClub" +[[nmi.credit]] + payment_method_type = "Discover" +[[nmi.credit]] + payment_method_type = "CartesBancaires" +[[nmi.credit]] + payment_method_type = "UnionPay" +[[nmi.debit]] + payment_method_type = "Mastercard" +[[nmi.debit]] + payment_method_type = "Visa" +[[nmi.debit]] + payment_method_type = "Interac" +[[nmi.debit]] + payment_method_type = "AmericanExpress" +[[nmi.debit]] + payment_method_type = "JCB" +[[nmi.debit]] + payment_method_type = "DinersClub" +[[nmi.debit]] + payment_method_type = "Discover" +[[nmi.debit]] + payment_method_type = "CartesBancaires" +[[nmi.debit]] + payment_method_type = "UnionPay" +[[nmi.bank_redirect]] + payment_method_type = "ideal" +[[nmi.wallet]] + payment_method_type = "apple_pay" +[[nmi.wallet]] + payment_method_type = "google_pay" +[nmi.connector_auth.BodyKey] +api_key="API Key" +key1="Public Key" +[nmi.connector_webhook_details] +merchant_secret="Source verification key" + +[nmi.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nmi.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nmi.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[noon] +[[noon.credit]] + payment_method_type = "Mastercard" +[[noon.credit]] + payment_method_type = "Visa" +[[noon.credit]] + payment_method_type = "Interac" +[[noon.credit]] + payment_method_type = "AmericanExpress" +[[noon.credit]] + payment_method_type = "JCB" +[[noon.credit]] + payment_method_type = "DinersClub" +[[noon.credit]] + payment_method_type = "Discover" +[[noon.credit]] + payment_method_type = "CartesBancaires" +[[noon.credit]] + payment_method_type = "UnionPay" +[[noon.debit]] + payment_method_type = "Mastercard" +[[noon.debit]] + payment_method_type = "Visa" +[[noon.debit]] + payment_method_type = "Interac" +[[noon.debit]] + payment_method_type = "AmericanExpress" +[[noon.debit]] + payment_method_type = "JCB" +[[noon.debit]] + payment_method_type = "DinersClub" +[[noon.debit]] + payment_method_type = "Discover" +[[noon.debit]] + payment_method_type = "CartesBancaires" +[[noon.debit]] + payment_method_type = "UnionPay" +[[noon.wallet]] + payment_method_type = "apple_pay" +[[noon.wallet]] + payment_method_type = "google_pay" +[[noon.wallet]] + payment_method_type = "paypal" +[noon.connector_auth.SignatureKey] +api_key="API Key" +key1="Business Identifier" +api_secret="Application Identifier" +[noon.connector_webhook_details] +merchant_secret="Source verification key" + +[noon.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[noon.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[noon.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[nuvei] +[[nuvei.credit]] + payment_method_type = "Mastercard" +[[nuvei.credit]] + payment_method_type = "Visa" +[[nuvei.credit]] + payment_method_type = "Interac" +[[nuvei.credit]] + payment_method_type = "AmericanExpress" +[[nuvei.credit]] + payment_method_type = "JCB" +[[nuvei.credit]] + payment_method_type = "DinersClub" +[[nuvei.credit]] + payment_method_type = "Discover" +[[nuvei.credit]] + payment_method_type = "CartesBancaires" +[[nuvei.credit]] + payment_method_type = "UnionPay" +[[nuvei.debit]] + payment_method_type = "Mastercard" +[[nuvei.debit]] + payment_method_type = "Visa" +[[nuvei.debit]] + payment_method_type = "Interac" +[[nuvei.debit]] + payment_method_type = "AmericanExpress" +[[nuvei.debit]] + payment_method_type = "JCB" +[[nuvei.debit]] + payment_method_type = "DinersClub" +[[nuvei.debit]] + payment_method_type = "Discover" +[[nuvei.debit]] + payment_method_type = "CartesBancaires" +[[nuvei.debit]] + payment_method_type = "UnionPay" +[[nuvei.pay_later]] + payment_method_type = "klarna" +[[nuvei.pay_later]] + payment_method_type = "afterpay_clearpay" +[[nuvei.bank_redirect]] + payment_method_type = "ideal" +[[nuvei.bank_redirect]] + payment_method_type = "giropay" +[[nuvei.bank_redirect]] + payment_method_type = "sofort" +[[nuvei.bank_redirect]] + payment_method_type = "eps" +[[nuvei.wallet]] + payment_method_type = "apple_pay" +[[nuvei.wallet]] + payment_method_type = "google_pay" +[[nuvei.wallet]] + payment_method_type = "paypal" +[nuvei.connector_auth.SignatureKey] +api_key="Merchant ID" +key1="Merchant Site ID" +api_secret="Merchant Secret" +[nuvei.connector_webhook_details] +merchant_secret="Source verification key" + +[nuvei.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nuvei.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nuvei.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + +[opennode] +[[opennode.crypto]] + payment_method_type = "crypto_currency" +[opennode.connector_auth.HeaderKey] +api_key="API Key" +[opennode.connector_webhook_details] +merchant_secret="Source verification key" + +[prophetpay] +[[prophetpay.card_redirect]] + payment_method_type = "card_redirect" +[prophetpay.connector_auth.SignatureKey] +api_key="Username" +key1="Token" +api_secret="Profile" + +[payme] +[[payme.credit]] + payment_method_type = "Mastercard" +[[payme.credit]] + payment_method_type = "Visa" +[[payme.credit]] + payment_method_type = "Interac" +[[payme.credit]] + payment_method_type = "AmericanExpress" +[[payme.credit]] + payment_method_type = "JCB" +[[payme.credit]] + payment_method_type = "DinersClub" +[[payme.credit]] + payment_method_type = "Discover" +[[payme.credit]] + payment_method_type = "CartesBancaires" +[[payme.credit]] + payment_method_type = "UnionPay" +[[payme.debit]] + payment_method_type = "Mastercard" +[[payme.debit]] + payment_method_type = "Visa" +[[payme.debit]] + payment_method_type = "Interac" +[[payme.debit]] + payment_method_type = "AmericanExpress" +[[payme.debit]] + payment_method_type = "JCB" +[[payme.debit]] + payment_method_type = "DinersClub" +[[payme.debit]] + payment_method_type = "Discover" +[[payme.debit]] + payment_method_type = "CartesBancaires" +[[payme.debit]] + payment_method_type = "UnionPay" +[payme.connector_auth.BodyKey] +api_key="Seller Payme Id" +key1="Payme Public Key" +[payme.connector_webhook_details] +merchant_secret="Payme Client Secret" +additional_secret="Payme Client Key" + +[paypal] +[[paypal.credit]] + payment_method_type = "Mastercard" +[[paypal.credit]] + payment_method_type = "Visa" +[[paypal.credit]] + payment_method_type = "Interac" +[[paypal.credit]] + payment_method_type = "AmericanExpress" +[[paypal.credit]] + payment_method_type = "JCB" +[[paypal.credit]] + payment_method_type = "DinersClub" +[[paypal.credit]] + payment_method_type = "Discover" +[[paypal.credit]] + payment_method_type = "CartesBancaires" +[[paypal.credit]] + payment_method_type = "UnionPay" +[[paypal.debit]] + payment_method_type = "Mastercard" +[[paypal.debit]] + payment_method_type = "Visa" +[[paypal.debit]] + payment_method_type = "Interac" +[[paypal.debit]] + payment_method_type = "AmericanExpress" +[[paypal.debit]] + payment_method_type = "JCB" +[[paypal.debit]] + payment_method_type = "DinersClub" +[[paypal.debit]] + payment_method_type = "Discover" +[[paypal.debit]] + payment_method_type = "CartesBancaires" +[[paypal.debit]] + payment_method_type = "UnionPay" +[[paypal.wallet]] + payment_method_type = "paypal" +[[paypal.bank_redirect]] + payment_method_type = "ideal" +[[paypal.bank_redirect]] + payment_method_type = "giropay" +[[paypal.bank_redirect]] + payment_method_type = "sofort" +[[paypal.bank_redirect]] + payment_method_type = "eps" +is_verifiable = true +[paypal.connector_auth.BodyKey] +api_key="Client Secret" +key1="Client ID" +[paypal.connector_webhook_details] +merchant_secret="Source verification key" + + +[payu] +[[payu.credit]] + payment_method_type = "Mastercard" +[[payu.credit]] + payment_method_type = "Visa" +[[payu.credit]] + payment_method_type = "Interac" +[[payu.credit]] + payment_method_type = "AmericanExpress" +[[payu.credit]] + payment_method_type = "JCB" +[[payu.credit]] + payment_method_type = "DinersClub" +[[payu.credit]] + payment_method_type = "Discover" +[[payu.credit]] + payment_method_type = "CartesBancaires" +[[payu.credit]] + payment_method_type = "UnionPay" +[[payu.debit]] + payment_method_type = "Mastercard" +[[payu.debit]] + payment_method_type = "Visa" +[[payu.debit]] + payment_method_type = "Interac" +[[payu.debit]] + payment_method_type = "AmericanExpress" +[[payu.debit]] + payment_method_type = "JCB" +[[payu.debit]] + payment_method_type = "DinersClub" +[[payu.debit]] + payment_method_type = "Discover" +[[payu.debit]] + payment_method_type = "CartesBancaires" +[[payu.debit]] + payment_method_type = "UnionPay" +[[payu.wallet]] + payment_method_type = "google_pay" +[payu.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant POS ID" +[payu.connector_webhook_details] +merchant_secret="Source verification key" + +[payu.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[placetopay] +[[placetopay.credit]] + payment_method_type = "Mastercard" +[[placetopay.credit]] + payment_method_type = "Visa" +[[placetopay.credit]] + payment_method_type = "Interac" +[[placetopay.credit]] + payment_method_type = "AmericanExpress" +[[placetopay.credit]] + payment_method_type = "JCB" +[[placetopay.credit]] + payment_method_type = "DinersClub" +[[placetopay.credit]] + payment_method_type = "Discover" +[[placetopay.credit]] + payment_method_type = "CartesBancaires" +[[placetopay.credit]] + payment_method_type = "UnionPay" +[[placetopay.debit]] + payment_method_type = "Mastercard" +[[placetopay.debit]] + payment_method_type = "Visa" +[[placetopay.debit]] + payment_method_type = "Interac" +[[placetopay.debit]] + payment_method_type = "AmericanExpress" +[[placetopay.debit]] + payment_method_type = "JCB" +[[placetopay.debit]] + payment_method_type = "DinersClub" +[[placetopay.debit]] + payment_method_type = "Discover" +[[placetopay.debit]] + payment_method_type = "CartesBancaires" +[[placetopay.debit]] + payment_method_type = "UnionPay" +[placetopay.connector_auth.BodyKey] +api_key="Login" +key1="Trankey" + +[powertranz] +[[powertranz.credit]] + payment_method_type = "Mastercard" +[[powertranz.credit]] + payment_method_type = "Visa" +[[powertranz.credit]] + payment_method_type = "Interac" +[[powertranz.credit]] + payment_method_type = "AmericanExpress" +[[powertranz.credit]] + payment_method_type = "JCB" +[[powertranz.credit]] + payment_method_type = "DinersClub" +[[powertranz.credit]] + payment_method_type = "Discover" +[[powertranz.credit]] + payment_method_type = "CartesBancaires" +[[powertranz.credit]] + payment_method_type = "UnionPay" +[[powertranz.debit]] + payment_method_type = "Mastercard" +[[powertranz.debit]] + payment_method_type = "Visa" +[[powertranz.debit]] + payment_method_type = "Interac" +[[powertranz.debit]] + payment_method_type = "AmericanExpress" +[[powertranz.debit]] + payment_method_type = "JCB" +[[powertranz.debit]] + payment_method_type = "DinersClub" +[[powertranz.debit]] + payment_method_type = "Discover" +[[powertranz.debit]] + payment_method_type = "CartesBancaires" +[[powertranz.debit]] + payment_method_type = "UnionPay" +[powertranz.connector_auth.BodyKey] +key1 = "PowerTranz Id" +api_key="PowerTranz Password" +[powertranz.connector_webhook_details] +merchant_secret="Source verification key" + +[rapyd] +[[rapyd.credit]] + payment_method_type = "Mastercard" +[[rapyd.credit]] + payment_method_type = "Visa" +[[rapyd.credit]] + payment_method_type = "Interac" +[[rapyd.credit]] + payment_method_type = "AmericanExpress" +[[rapyd.credit]] + payment_method_type = "JCB" +[[rapyd.credit]] + payment_method_type = "DinersClub" +[[rapyd.credit]] + payment_method_type = "Discover" +[[rapyd.credit]] + payment_method_type = "CartesBancaires" +[[rapyd.credit]] + payment_method_type = "UnionPay" +[[rapyd.debit]] + payment_method_type = "Mastercard" +[[rapyd.debit]] + payment_method_type = "Visa" +[[rapyd.debit]] + payment_method_type = "Interac" +[[rapyd.debit]] + payment_method_type = "AmericanExpress" +[[rapyd.debit]] + payment_method_type = "JCB" +[[rapyd.debit]] + payment_method_type = "DinersClub" +[[rapyd.debit]] + payment_method_type = "Discover" +[[rapyd.debit]] + payment_method_type = "CartesBancaires" +[[rapyd.debit]] + payment_method_type = "UnionPay" +[[rapyd.wallet]] + payment_method_type = "apple_pay" +[rapyd.connector_auth.BodyKey] +api_key="Access Key" +key1="API Secret" +[rapyd.connector_webhook_details] +merchant_secret="Source verification key" + +[rapyd.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[rapyd.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[shift4] +[[shift4.credit]] + payment_method_type = "Mastercard" +[[shift4.credit]] + payment_method_type = "Visa" +[[shift4.credit]] + payment_method_type = "Interac" +[[shift4.credit]] + payment_method_type = "AmericanExpress" +[[shift4.credit]] + payment_method_type = "JCB" +[[shift4.credit]] + payment_method_type = "DinersClub" +[[shift4.credit]] + payment_method_type = "Discover" +[[shift4.credit]] + payment_method_type = "CartesBancaires" +[[shift4.credit]] + payment_method_type = "UnionPay" +[[shift4.debit]] + payment_method_type = "Mastercard" +[[shift4.debit]] + payment_method_type = "Visa" +[[shift4.debit]] + payment_method_type = "Interac" +[[shift4.debit]] + payment_method_type = "AmericanExpress" +[[shift4.debit]] + payment_method_type = "JCB" +[[shift4.debit]] + payment_method_type = "DinersClub" +[[shift4.debit]] + payment_method_type = "Discover" +[[shift4.debit]] + payment_method_type = "CartesBancaires" +[[shift4.debit]] + payment_method_type = "UnionPay" +[[shift4.bank_redirect]] + payment_method_type = "ideal" +[[shift4.bank_redirect]] + payment_method_type = "giropay" +[[shift4.bank_redirect]] + payment_method_type = "sofort" +[[shift4.bank_redirect]] + payment_method_type = "eps" +[shift4.connector_auth.HeaderKey] +api_key="API Key" +[shift4.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe] +[[stripe.credit]] + payment_method_type = "Mastercard" +[[stripe.credit]] + payment_method_type = "Visa" +[[stripe.credit]] + payment_method_type = "Interac" +[[stripe.credit]] + payment_method_type = "AmericanExpress" +[[stripe.credit]] + payment_method_type = "JCB" +[[stripe.credit]] + payment_method_type = "DinersClub" +[[stripe.credit]] + payment_method_type = "Discover" +[[stripe.credit]] + payment_method_type = "CartesBancaires" +[[stripe.credit]] + payment_method_type = "UnionPay" +[[stripe.debit]] + payment_method_type = "Mastercard" +[[stripe.debit]] + payment_method_type = "Visa" +[[stripe.debit]] + payment_method_type = "Interac" +[[stripe.debit]] + payment_method_type = "AmericanExpress" +[[stripe.debit]] + payment_method_type = "JCB" +[[stripe.debit]] + payment_method_type = "DinersClub" +[[stripe.debit]] + payment_method_type = "Discover" +[[stripe.debit]] + payment_method_type = "CartesBancaires" +[[stripe.debit]] + payment_method_type = "UnionPay" +[[stripe.pay_later]] + payment_method_type = "klarna" +[[stripe.pay_later]] + payment_method_type = "affirm" +[[stripe.pay_later]] + payment_method_type = "afterpay_clearpay" +[[stripe.bank_redirect]] + payment_method_type = "ideal" +[[stripe.bank_redirect]] + payment_method_type = "giropay" +[[stripe.bank_redirect]] + payment_method_type = "sofort" +[[stripe.bank_redirect]] + payment_method_type = "eps" +[[stripe.bank_redirect]] + payment_method_type = "bancontact_card" +[[stripe.bank_redirect]] + payment_method_type = "przelewy24" +[[stripe.bank_debit]] + payment_method_type = "ach" +[[stripe.bank_debit]] + payment_method_type = "bacs" +[[stripe.bank_debit]] + payment_method_type = "becs" +[[stripe.bank_debit]] + payment_method_type = "sepa" +[[stripe.bank_transfer]] + payment_method_type = "ach" +[[stripe.bank_transfer]] + payment_method_type = "bacs" +[[stripe.bank_transfer]] + payment_method_type = "sepa" +[[stripe.bank_transfer]] + payment_method_type = "multibanco" +[[stripe.wallet]] + payment_method_type = "apple_pay" +[[stripe.wallet]] + payment_method_type = "google_pay" +[[stripe.wallet]] + payment_method_type = "we_chat_pay" +[[stripe.wallet]] + payment_method_type = "ali_pay" +[[stripe.wallet]] + payment_method_type = "cashapp" +is_verifiable = true +[stripe.connector_auth.HeaderKey] +api_key="Secret Key" +[stripe.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +stripe_publishable_key="Stripe Publishable Key" +merchant_id="Google Pay Merchant ID" + +[stripe.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[stripe.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[stax] +[[stax.credit]] + payment_method_type = "Mastercard" +[[stax.credit]] + payment_method_type = "Visa" +[[stax.credit]] + payment_method_type = "Interac" +[[stax.credit]] + payment_method_type = "AmericanExpress" +[[stax.credit]] + payment_method_type = "JCB" +[[stax.credit]] + payment_method_type = "DinersClub" +[[stax.credit]] + payment_method_type = "Discover" +[[stax.credit]] + payment_method_type = "CartesBancaires" +[[stax.credit]] + payment_method_type = "UnionPay" +[[stax.debit]] + payment_method_type = "Mastercard" +[[stax.debit]] + payment_method_type = "Visa" +[[stax.debit]] + payment_method_type = "Interac" +[[stax.debit]] + payment_method_type = "AmericanExpress" +[[stax.debit]] + payment_method_type = "JCB" +[[stax.debit]] + payment_method_type = "DinersClub" +[[stax.debit]] + payment_method_type = "Discover" +[[stax.debit]] + payment_method_type = "CartesBancaires" +[[stax.debit]] + payment_method_type = "UnionPay" +[[stax.bank_debit]] + payment_method_type = "ach" +[stax.connector_auth.HeaderKey] +api_key="Api Key" +[stax.connector_webhook_details] +merchant_secret="Source verification key" + +[square] +[[square.credit]] + payment_method_type = "Mastercard" +[[square.credit]] + payment_method_type = "Visa" +[[square.credit]] + payment_method_type = "Interac" +[[square.credit]] + payment_method_type = "AmericanExpress" +[[square.credit]] + payment_method_type = "JCB" +[[square.credit]] + payment_method_type = "DinersClub" +[[square.credit]] + payment_method_type = "Discover" +[[square.credit]] + payment_method_type = "CartesBancaires" +[[square.credit]] + payment_method_type = "UnionPay" +[[square.debit]] + payment_method_type = "Mastercard" +[[square.debit]] + payment_method_type = "Visa" +[[square.debit]] + payment_method_type = "Interac" +[[square.debit]] + payment_method_type = "AmericanExpress" +[[square.debit]] + payment_method_type = "JCB" +[[square.debit]] + payment_method_type = "DinersClub" +[[square.debit]] + payment_method_type = "Discover" +[[square.debit]] + payment_method_type = "CartesBancaires" +[[square.debit]] + payment_method_type = "UnionPay" +[square_payout.connector_auth.BodyKey] +api_key = "Square API Key" +key1 = "Square Client Id" +[square.connector_webhook_details] +merchant_secret="Source verification key" + +[trustpay] +[[trustpay.credit]] + payment_method_type = "Mastercard" +[[trustpay.credit]] + payment_method_type = "Visa" +[[trustpay.credit]] + payment_method_type = "Interac" +[[trustpay.credit]] + payment_method_type = "AmericanExpress" +[[trustpay.credit]] + payment_method_type = "JCB" +[[trustpay.credit]] + payment_method_type = "DinersClub" +[[trustpay.credit]] + payment_method_type = "Discover" +[[trustpay.credit]] + payment_method_type = "CartesBancaires" +[[trustpay.credit]] + payment_method_type = "UnionPay" +[[trustpay.debit]] + payment_method_type = "Mastercard" +[[trustpay.debit]] + payment_method_type = "Visa" +[[trustpay.debit]] + payment_method_type = "Interac" +[[trustpay.debit]] + payment_method_type = "AmericanExpress" +[[trustpay.debit]] + payment_method_type = "JCB" +[[trustpay.debit]] + payment_method_type = "DinersClub" +[[trustpay.debit]] + payment_method_type = "Discover" +[[trustpay.debit]] + payment_method_type = "CartesBancaires" +[[trustpay.debit]] + payment_method_type = "UnionPay" +[[trustpay.bank_redirect]] + payment_method_type = "ideal" +[[trustpay.bank_redirect]] + payment_method_type = "giropay" +[[trustpay.bank_redirect]] + payment_method_type = "sofort" +[[trustpay.bank_redirect]] + payment_method_type = "eps" +[[trustpay.bank_redirect]] + payment_method_type = "blik" +[[trustpay.wallet]] + payment_method_type = "apple_pay" +[[trustpay.wallet]] + payment_method_type = "google_pay" +[trustpay.connector_auth.SignatureKey] +api_key="API Key" +key1="Project ID" +api_secret="Secret Key" +[trustpay.connector_webhook_details] +merchant_secret="Source verification key" + +[trustpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[trustpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[tsys] +[[tsys.credit]] + payment_method_type = "Mastercard" +[[tsys.credit]] + payment_method_type = "Visa" +[[tsys.credit]] + payment_method_type = "Interac" +[[tsys.credit]] + payment_method_type = "AmericanExpress" +[[tsys.credit]] + payment_method_type = "JCB" +[[tsys.credit]] + payment_method_type = "DinersClub" +[[tsys.credit]] + payment_method_type = "Discover" +[[tsys.credit]] + payment_method_type = "CartesBancaires" +[[tsys.credit]] + payment_method_type = "UnionPay" +[[tsys.debit]] + payment_method_type = "Mastercard" +[[tsys.debit]] + payment_method_type = "Visa" +[[tsys.debit]] + payment_method_type = "Interac" +[[tsys.debit]] + payment_method_type = "AmericanExpress" +[[tsys.debit]] + payment_method_type = "JCB" +[[tsys.debit]] + payment_method_type = "DinersClub" +[[tsys.debit]] + payment_method_type = "Discover" +[[tsys.debit]] + payment_method_type = "CartesBancaires" +[[tsys.debit]] + payment_method_type = "UnionPay" +[tsys.connector_auth.SignatureKey] +api_key="Device Id" +key1="Transaction Key" +api_secret="Developer Id" +[tsys.connector_webhook_details] +merchant_secret="Source verification key" + +[volt] +[[volt.bank_redirect]] + payment_method_type = "open_banking_uk" +[volt.connector_auth.MultiAuthKey] +api_key = "Username" +api_secret = "Password" +key1 = "Client ID" +key2 = "Client Secret" +[volt.connector_webhook_details] +merchant_secret="Source verification key" + +[worldline] +[[worldline.credit]] + payment_method_type = "Mastercard" +[[worldline.credit]] + payment_method_type = "Visa" +[[worldline.credit]] + payment_method_type = "Interac" +[[worldline.credit]] + payment_method_type = "AmericanExpress" +[[worldline.credit]] + payment_method_type = "JCB" +[[worldline.credit]] + payment_method_type = "DinersClub" +[[worldline.credit]] + payment_method_type = "Discover" +[[worldline.credit]] + payment_method_type = "CartesBancaires" +[[worldline.credit]] + payment_method_type = "UnionPay" +[[worldline.debit]] + payment_method_type = "Mastercard" +[[worldline.debit]] + payment_method_type = "Visa" +[[worldline.debit]] + payment_method_type = "Interac" +[[worldline.debit]] + payment_method_type = "AmericanExpress" +[[worldline.debit]] + payment_method_type = "JCB" +[[worldline.debit]] + payment_method_type = "DinersClub" +[[worldline.debit]] + payment_method_type = "Discover" +[[worldline.debit]] + payment_method_type = "CartesBancaires" +[[worldline.debit]] + payment_method_type = "UnionPay" +[[worldline.bank_redirect]] + payment_method_type = "ideal" +[[worldline.bank_redirect]] + payment_method_type = "giropay" +[worldline.connector_auth.SignatureKey] +api_key="API Key ID" +key1="Merchant ID" +api_secret="Secret API Key" +[worldline.connector_webhook_details] +merchant_secret="Source verification key" + +[worldpay] +[[worldpay.credit]] + payment_method_type = "Mastercard" +[[worldpay.credit]] + payment_method_type = "Visa" +[[worldpay.credit]] + payment_method_type = "Interac" +[[worldpay.credit]] + payment_method_type = "AmericanExpress" +[[worldpay.credit]] + payment_method_type = "JCB" +[[worldpay.credit]] + payment_method_type = "DinersClub" +[[worldpay.credit]] + payment_method_type = "Discover" +[[worldpay.credit]] + payment_method_type = "CartesBancaires" +[[worldpay.credit]] + payment_method_type = "UnionPay" +[[worldpay.debit]] + payment_method_type = "Mastercard" +[[worldpay.debit]] + payment_method_type = "Visa" +[[worldpay.debit]] + payment_method_type = "Interac" +[[worldpay.debit]] + payment_method_type = "AmericanExpress" +[[worldpay.debit]] + payment_method_type = "JCB" +[[worldpay.debit]] + payment_method_type = "DinersClub" +[[worldpay.debit]] + payment_method_type = "Discover" +[[worldpay.debit]] + payment_method_type = "CartesBancaires" +[[worldpay.debit]] + payment_method_type = "UnionPay" +[[worldpay.wallet]] + payment_method_type = "google_pay" +[[worldpay.wallet]] + payment_method_type = "apple_pay" +[worldpay.connector_auth.BodyKey] +api_key="Username" +key1="Password" +[worldpay.connector_webhook_details] +merchant_secret="Source verification key" + +[worldpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[worldpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[worldpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[zen] +[[zen.credit]] + payment_method_type = "Mastercard" +[[zen.credit]] + payment_method_type = "Visa" +[[zen.credit]] + payment_method_type = "Interac" +[[zen.credit]] + payment_method_type = "AmericanExpress" +[[zen.credit]] + payment_method_type = "JCB" +[[zen.credit]] + payment_method_type = "DinersClub" +[[zen.credit]] + payment_method_type = "Discover" +[[zen.credit]] + payment_method_type = "CartesBancaires" +[[zen.credit]] + payment_method_type = "UnionPay" +[[zen.debit]] + payment_method_type = "Mastercard" +[[zen.debit]] + payment_method_type = "Visa" +[[zen.debit]] + payment_method_type = "Interac" +[[zen.debit]] + payment_method_type = "AmericanExpress" +[[zen.debit]] + payment_method_type = "JCB" +[[zen.debit]] + payment_method_type = "DinersClub" +[[zen.debit]] + payment_method_type = "Discover" +[[zen.debit]] + payment_method_type = "CartesBancaires" +[[zen.debit]] + payment_method_type = "UnionPay" +[[zen.voucher]] + payment_method_type = "boleto" +[[zen.voucher]] + payment_method_type = "efecty" +[[zen.voucher]] + payment_method_type = "pago_efectivo" +[[zen.voucher]] + payment_method_type = "red_compra" +[[zen.voucher]] + payment_method_type = "red_pagos" +[[zen.bank_transfer]] + payment_method_type = "pix" +[[zen.bank_transfer]] + payment_method_type = "pse" +[[zen.wallet]] + payment_method_type = "apple_pay" +[[zen.wallet]] + payment_method_type = "google_pay" +[zen.connector_auth.HeaderKey] +api_key="API Key" +[zen.connector_webhook_details] +merchant_secret="Source verification key" + +[zen.metadata.apple_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" +[zen.metadata.google_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" + + +[dummy_connector] +[[dummy_connector.credit]] + payment_method_type = "Mastercard" +[[dummy_connector.credit]] + payment_method_type = "Visa" +[[dummy_connector.credit]] + payment_method_type = "Interac" +[[dummy_connector.credit]] + payment_method_type = "AmericanExpress" +[[dummy_connector.credit]] + payment_method_type = "JCB" +[[dummy_connector.credit]] + payment_method_type = "DinersClub" +[[dummy_connector.credit]] + payment_method_type = "Discover" +[[dummy_connector.credit]] + payment_method_type = "CartesBancaires" +[[dummy_connector.credit]] + payment_method_type = "UnionPay" +[[dummy_connector.debit]] + payment_method_type = "Mastercard" +[[dummy_connector.debit]] + payment_method_type = "Visa" +[[dummy_connector.debit]] + payment_method_type = "Interac" +[[dummy_connector.debit]] + payment_method_type = "AmericanExpress" +[[dummy_connector.debit]] + payment_method_type = "JCB" +[[dummy_connector.debit]] + payment_method_type = "DinersClub" +[[dummy_connector.debit]] + payment_method_type = "Discover" +[[dummy_connector.debit]] + payment_method_type = "CartesBancaires" +[[dummy_connector.debit]] + payment_method_type = "UnionPay" +[dummy_connector.connector_auth.HeaderKey] +api_key="Api Key" + +[paypal_test] +[[paypal_test.credit]] + payment_method_type = "Mastercard" +[[paypal_test.credit]] + payment_method_type = "Visa" +[[paypal_test.credit]] + payment_method_type = "Interac" +[[paypal_test.credit]] + payment_method_type = "AmericanExpress" +[[paypal_test.credit]] + payment_method_type = "JCB" +[[paypal_test.credit]] + payment_method_type = "DinersClub" +[[paypal_test.credit]] + payment_method_type = "Discover" +[[paypal_test.credit]] + payment_method_type = "CartesBancaires" +[[paypal_test.credit]] + payment_method_type = "UnionPay" +[[paypal_test.debit]] + payment_method_type = "Mastercard" +[[paypal_test.debit]] + payment_method_type = "Visa" +[[paypal_test.debit]] + payment_method_type = "Interac" +[[paypal_test.debit]] + payment_method_type = "AmericanExpress" +[[paypal_test.debit]] + payment_method_type = "JCB" +[[paypal_test.debit]] + payment_method_type = "DinersClub" +[[paypal_test.debit]] + payment_method_type = "Discover" +[[paypal_test.debit]] + payment_method_type = "CartesBancaires" +[[paypal_test.debit]] + payment_method_type = "UnionPay" +[[paypal_test.wallet]] + payment_method_type = "paypal" +[paypal_test.connector_auth.HeaderKey] +api_key="Api Key" + +[stripe_test] +[[stripe_test.credit]] + payment_method_type = "Mastercard" +[[stripe_test.credit]] + payment_method_type = "Visa" +[[stripe_test.credit]] + payment_method_type = "Interac" +[[stripe_test.credit]] + payment_method_type = "AmericanExpress" +[[stripe_test.credit]] + payment_method_type = "JCB" +[[stripe_test.credit]] + payment_method_type = "DinersClub" +[[stripe_test.credit]] + payment_method_type = "Discover" +[[stripe_test.credit]] + payment_method_type = "CartesBancaires" +[[stripe_test.credit]] + payment_method_type = "UnionPay" +[[stripe_test.debit]] + payment_method_type = "Mastercard" +[[stripe_test.debit]] + payment_method_type = "Visa" +[[stripe_test.debit]] + payment_method_type = "Interac" +[[stripe_test.debit]] + payment_method_type = "AmericanExpress" +[[stripe_test.debit]] + payment_method_type = "JCB" +[[stripe_test.debit]] + payment_method_type = "DinersClub" +[[stripe_test.debit]] + payment_method_type = "Discover" +[[stripe_test.debit]] + payment_method_type = "CartesBancaires" +[[stripe_test.debit]] + payment_method_type = "UnionPay" +[[stripe_test.wallet]] + payment_method_type = "google_pay" +[[stripe_test.wallet]] + payment_method_type = "ali_pay" +[[stripe_test.wallet]] + payment_method_type = "we_chat_pay" +[[stripe_test.pay_later]] + payment_method_type = "klarna" +[[stripe_test.pay_later]] + payment_method_type = "affirm" +[[stripe_test.pay_later]] + payment_method_type = "afterpay_clearpay" +[stripe_test.connector_auth.HeaderKey] +api_key="Api Key" + +[helcim] +[[helcim.credit]] + payment_method_type = "Mastercard" +[[helcim.credit]] + payment_method_type = "Visa" +[[helcim.credit]] + payment_method_type = "Interac" +[[helcim.credit]] + payment_method_type = "AmericanExpress" +[[helcim.credit]] + payment_method_type = "JCB" +[[helcim.credit]] + payment_method_type = "DinersClub" +[[helcim.credit]] + payment_method_type = "Discover" +[[helcim.credit]] + payment_method_type = "CartesBancaires" +[[helcim.credit]] + payment_method_type = "UnionPay" +[[helcim.debit]] + payment_method_type = "Mastercard" +[[helcim.debit]] + payment_method_type = "Visa" +[[helcim.debit]] + payment_method_type = "Interac" +[[helcim.debit]] + payment_method_type = "AmericanExpress" +[[helcim.debit]] + payment_method_type = "JCB" +[[helcim.debit]] + payment_method_type = "DinersClub" +[[helcim.debit]] + payment_method_type = "Discover" +[[helcim.debit]] + payment_method_type = "CartesBancaires" +[[helcim.debit]] + payment_method_type = "UnionPay" +[helcim.connector_auth.HeaderKey] +api_key="Api Key" + + + + +[adyen_payout] +[[adyen_payout.credit]] + payment_method_type = "Mastercard" +[[adyen_payout.credit]] + payment_method_type = "Visa" +[[adyen_payout.credit]] + payment_method_type = "Interac" +[[adyen_payout.credit]] + payment_method_type = "AmericanExpress" +[[adyen_payout.credit]] + payment_method_type = "JCB" +[[adyen_payout.credit]] + payment_method_type = "DinersClub" +[[adyen_payout.credit]] + payment_method_type = "Discover" +[[adyen_payout.credit]] + payment_method_type = "CartesBancaires" +[[adyen_payout.credit]] + payment_method_type = "UnionPay" +[[adyen_payout.debit]] + payment_method_type = "Mastercard" +[[adyen_payout.debit]] + payment_method_type = "Visa" +[[adyen_payout.debit]] + payment_method_type = "Interac" +[[adyen_payout.debit]] + payment_method_type = "AmericanExpress" +[[adyen_payout.debit]] + payment_method_type = "JCB" +[[adyen_payout.debit]] + payment_method_type = "DinersClub" +[[adyen_payout.debit]] + payment_method_type = "Discover" +[[adyen_payout.debit]] + payment_method_type = "CartesBancaires" +[[adyen_payout.debit]] + payment_method_type = "UnionPay" +[[adyen_payout.bank_transfer]] + payment_method_type = "ach" +[[adyen_payout.bank_transfer]] + payment_method_type = "bacs" +[[adyen_payout.bank_transfer]] + payment_method_type = "sepa" +[adyen_payout.connector_auth.SignatureKey] +api_key = "Adyen API Key (Payout creation)" +api_secret = "Adyen Key (Payout submission)" +key1 = "Adyen Account Id" + +[wise_payout] +[[wise_payout.bank_transfer]] + payment_method_type = "ach" +[[wise_payout.bank_transfer]] + payment_method_type = "bacs" +[[wise_payout.bank_transfer]] + payment_method_type = "sepa" +[wise_payout.connector_auth.BodyKey] +api_key = "Wise API Key" +key1 = "Wise Account Id" \ No newline at end of file diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml new file mode 100644 index 000000000000..04e86b5a8792 --- /dev/null +++ b/crates/connector_configs/toml/production.toml @@ -0,0 +1,1849 @@ +[aci] +[[aci.credit]] + payment_method_type = "Mastercard" +[[aci.credit]] + payment_method_type = "Visa" +[[aci.credit]] + payment_method_type = "Interac" +[[aci.credit]] + payment_method_type = "AmericanExpress" +[[aci.credit]] + payment_method_type = "JCB" +[[aci.credit]] + payment_method_type = "DinersClub" +[[aci.credit]] + payment_method_type = "Discover" +[[aci.credit]] + payment_method_type = "CartesBancaires" +[[aci.credit]] + payment_method_type = "UnionPay" +[[aci.debit]] + payment_method_type = "Mastercard" +[[aci.debit]] + payment_method_type = "Visa" +[[aci.debit]] + payment_method_type = "Interac" +[[aci.debit]] + payment_method_type = "AmericanExpress" +[[aci.debit]] + payment_method_type = "JCB" +[[aci.debit]] + payment_method_type = "DinersClub" +[[aci.debit]] + payment_method_type = "Discover" +[[aci.debit]] + payment_method_type = "CartesBancaires" +[[aci.debit]] + payment_method_type = "UnionPay" +[[aci.wallet]] + payment_method_type = "ali_pay" +[[aci.wallet]] + payment_method_type = "mb_way" +[[aci.bank_redirect]] + payment_method_type = "ideal" +[[aci.bank_redirect]] + payment_method_type = "giropay" +[[aci.bank_redirect]] + payment_method_type = "sofort" +[[aci.bank_redirect]] + payment_method_type = "eps" +[[aci.bank_redirect]] + payment_method_type = "przelewy24" +[[aci.bank_redirect]] + payment_method_type = "trustly" +[aci.connector_auth.BodyKey] +api_key="API Key" +key1="Entity ID" +[aci.connector_webhook_details] +merchant_secret="Source verification key" + +[adyen] +[[adyen.credit]] + payment_method_type = "Mastercard" +[[adyen.credit]] + payment_method_type = "Visa" +[[adyen.credit]] + payment_method_type = "Interac" +[[adyen.credit]] + payment_method_type = "AmericanExpress" +[[adyen.credit]] + payment_method_type = "JCB" +[[adyen.credit]] + payment_method_type = "DinersClub" +[[adyen.credit]] + payment_method_type = "Discover" +[[adyen.credit]] + payment_method_type = "CartesBancaires" +[[adyen.credit]] + payment_method_type = "UnionPay" +[[adyen.debit]] + payment_method_type = "Mastercard" +[[adyen.debit]] + payment_method_type = "Visa" +[[adyen.debit]] + payment_method_type = "Interac" +[[adyen.debit]] + payment_method_type = "AmericanExpress" +[[adyen.debit]] + payment_method_type = "JCB" +[[adyen.debit]] + payment_method_type = "DinersClub" +[[adyen.debit]] + payment_method_type = "Discover" +[[adyen.debit]] + payment_method_type = "CartesBancaires" +[[adyen.debit]] + payment_method_type = "UnionPay" +[[adyen.pay_later]] + payment_method_type = "klarna" +[[adyen.pay_later]] + payment_method_type = "affirm" +[[adyen.pay_later]] + payment_method_type = "afterpay_clearpay" +[[adyen.bank_debit]] + payment_method_type = "ach" +[[adyen.bank_debit]] + payment_method_type = "bacs" +[[adyen.bank_redirect]] + payment_method_type = "ideal" +[[adyen.bank_redirect]] + payment_method_type = "giropay" +[[adyen.bank_redirect]] + payment_method_type = "sofort" +[[adyen.bank_redirect]] + payment_method_type = "eps" +[[adyen.wallet]] + payment_method_type = "apple_pay" +[[adyen.wallet]] + payment_method_type = "google_pay" +[[adyen.wallet]] + payment_method_type = "paypal" +[adyen.connector_auth.BodyKey] +api_key="Adyen API Key" +key1="Adyen Account Id" +[adyen.connector_webhook_details] +merchant_secret="Source verification key" + +[adyen.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[adyen.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[adyen.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + + +[airwallex] +[[airwallex.credit]] + payment_method_type = "Mastercard" +[[airwallex.credit]] + payment_method_type = "Visa" +[[airwallex.credit]] + payment_method_type = "Interac" +[[airwallex.credit]] + payment_method_type = "AmericanExpress" +[[airwallex.credit]] + payment_method_type = "JCB" +[[airwallex.credit]] + payment_method_type = "DinersClub" +[[airwallex.credit]] + payment_method_type = "Discover" +[[airwallex.credit]] + payment_method_type = "CartesBancaires" +[[airwallex.credit]] + payment_method_type = "UnionPay" +[[airwallex.debit]] + payment_method_type = "Mastercard" +[[airwallex.debit]] + payment_method_type = "Visa" +[[airwallex.debit]] + payment_method_type = "Interac" +[[airwallex.debit]] + payment_method_type = "AmericanExpress" +[[airwallex.debit]] + payment_method_type = "JCB" +[[airwallex.debit]] + payment_method_type = "DinersClub" +[[airwallex.debit]] + payment_method_type = "Discover" +[[airwallex.debit]] + payment_method_type = "CartesBancaires" +[[airwallex.debit]] + payment_method_type = "UnionPay" +body_type="BodyKey" +[airwallex.connector_auth.BodyKey] +api_key="API Key" +key1="Client ID" +[airwallex.connector_webhook_details] +merchant_secret="Source verification key" + +[authorizedotnet] +[[authorizedotnet.credit]] + payment_method_type = "Mastercard" +[[authorizedotnet.credit]] + payment_method_type = "Visa" +[[authorizedotnet.credit]] + payment_method_type = "Interac" +[[authorizedotnet.credit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.credit]] + payment_method_type = "JCB" +[[authorizedotnet.credit]] + payment_method_type = "DinersClub" +[[authorizedotnet.credit]] + payment_method_type = "Discover" +[[authorizedotnet.credit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.credit]] + payment_method_type = "UnionPay" +[[authorizedotnet.debit]] + payment_method_type = "Mastercard" +[[authorizedotnet.debit]] + payment_method_type = "Visa" +[[authorizedotnet.debit]] + payment_method_type = "Interac" +[[authorizedotnet.debit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.debit]] + payment_method_type = "JCB" +[[authorizedotnet.debit]] + payment_method_type = "DinersClub" +[[authorizedotnet.debit]] + payment_method_type = "Discover" +[[authorizedotnet.debit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.debit]] + payment_method_type = "UnionPay" +[[authorizedotnet.wallet]] + payment_method_type = "google_pay" +[[authorizedotnet.wallet]] + payment_method_type = "paypal" +body_type="BodyKey" +[authorizedotnet.connector_auth.BodyKey] +api_key="API Login ID" +key1="Transaction Key" +[authorizedotnet.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" +[authorizedotnet.connector_webhook_details] +merchant_secret="Source verification key" + +[bitpay] +[[bitpay.crypto]] + payment_method_type = "crypto_currency" +[bitpay.connector_auth.HeaderKey] +api_key="API Key" +[bitpay.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap] +[[bluesnap.credit]] + payment_method_type = "Mastercard" +[[bluesnap.credit]] + payment_method_type = "Visa" +[[bluesnap.credit]] + payment_method_type = "Interac" +[[bluesnap.credit]] + payment_method_type = "AmericanExpress" +[[bluesnap.credit]] + payment_method_type = "JCB" +[[bluesnap.credit]] + payment_method_type = "DinersClub" +[[bluesnap.credit]] + payment_method_type = "Discover" +[[bluesnap.credit]] + payment_method_type = "CartesBancaires" +[[bluesnap.credit]] + payment_method_type = "UnionPay" +[[bluesnap.debit]] + payment_method_type = "Mastercard" +[[bluesnap.debit]] + payment_method_type = "Visa" +[[bluesnap.debit]] + payment_method_type = "Interac" +[[bluesnap.debit]] + payment_method_type = "AmericanExpress" +[[bluesnap.debit]] + payment_method_type = "JCB" +[[bluesnap.debit]] + payment_method_type = "DinersClub" +[[bluesnap.debit]] + payment_method_type = "Discover" +[[bluesnap.debit]] + payment_method_type = "CartesBancaires" +[[bluesnap.debit]] + payment_method_type = "UnionPay" +[[bluesnap.wallet]] + payment_method_type = "google_pay" +[[bluesnap.wallet]] + payment_method_type = "apple_pay" +[bluesnap.connector_auth.BodyKey] +api_key="Password" +key1="Username" +[bluesnap.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[bluesnap.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bluesnap.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" +[bluesnap.metadata] +merchant_id="Merchant Id" + +[braintree] +[[braintree.credit]] + payment_method_type = "Mastercard" +[[braintree.credit]] + payment_method_type = "Visa" +[[braintree.credit]] + payment_method_type = "Interac" +[[braintree.credit]] + payment_method_type = "AmericanExpress" +[[braintree.credit]] + payment_method_type = "JCB" +[[braintree.credit]] + payment_method_type = "DinersClub" +[[braintree.credit]] + payment_method_type = "Discover" +[[braintree.credit]] + payment_method_type = "CartesBancaires" +[[braintree.credit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "Mastercard" +[[braintree.debit]] + payment_method_type = "Visa" +[[braintree.debit]] + payment_method_type = "Interac" +[[braintree.debit]] + payment_method_type = "AmericanExpress" +[[braintree.debit]] + payment_method_type = "JCB" +[[braintree.debit]] + payment_method_type = "DinersClub" +[[braintree.debit]] + payment_method_type = "Discover" +[[braintree.debit]] + payment_method_type = "CartesBancaires" +[[bluesnap.debit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "UnionPay" + +[braintree.connector_auth.SignatureKey] +api_key="Public Key" +key1="Merchant Id" +api_secret="Private Key" +[braintree.connector_webhook_details] +merchant_secret="Source verification key" +[braintree.metadata] +merchant_account_id="Merchant Account Id" +merchant_config_currency="Currency" + +[bambora] +[[bambora.credit]] + payment_method_type = "Mastercard" +[[bambora.credit]] + payment_method_type = "Visa" +[[bambora.credit]] + payment_method_type = "Interac" +[[bambora.credit]] + payment_method_type = "AmericanExpress" +[[bambora.credit]] + payment_method_type = "JCB" +[[bambora.credit]] + payment_method_type = "DinersClub" +[[bambora.credit]] + payment_method_type = "Discover" +[[bambora.credit]] + payment_method_type = "CartesBancaires" +[[bambora.credit]] + payment_method_type = "UnionPay" +[[bambora.debit]] + payment_method_type = "Mastercard" +[[bambora.debit]] + payment_method_type = "Visa" +[[bambora.debit]] + payment_method_type = "Interac" +[[bambora.debit]] + payment_method_type = "AmericanExpress" +[[bambora.debit]] + payment_method_type = "JCB" +[[bambora.debit]] + payment_method_type = "DinersClub" +[[bambora.debit]] + payment_method_type = "Discover" +[[bambora.debit]] + payment_method_type = "CartesBancaires" +[[bambora.debit]] + payment_method_type = "UnionPay" +[[bambora.wallet]] + payment_method_type = "apple_pay" +[[bambora.wallet]] + payment_method_type = "paypal" +[bambora.connector_auth.BodyKey] +api_key="Passcode" +key1="Merchant Id" +[bambora.connector_webhook_details] +merchant_secret="Source verification key" + +[bambora.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bambora.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica] +[[bankofamerica.credit]] + payment_method_type = "Mastercard" +[[bankofamerica.credit]] + payment_method_type = "Visa" +[[bankofamerica.credit]] + payment_method_type = "Interac" +[[bankofamerica.credit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.credit]] + payment_method_type = "JCB" +[[bankofamerica.credit]] + payment_method_type = "DinersClub" +[[bankofamerica.credit]] + payment_method_type = "Discover" +[[bankofamerica.credit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.credit]] + payment_method_type = "UnionPay" +[[bankofamerica.debit]] + payment_method_type = "Mastercard" +[[bankofamerica.debit]] + payment_method_type = "Visa" +[[bankofamerica.debit]] + payment_method_type = "Interac" +[[bankofamerica.debit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.debit]] + payment_method_type = "JCB" +[[bankofamerica.debit]] + payment_method_type = "DinersClub" +[[bankofamerica.debit]] + payment_method_type = "Discover" +[[bankofamerica.debit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.debit]] + payment_method_type = "UnionPay" +[[bankofamerica.wallet]] + payment_method_type = "apple_pay" +[[bankofamerica.wallet]] + payment_method_type = "google_pay" + +[bankofamerica.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[bankofamerica.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bankofamerica.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[cashtocode] +[[cashtocode.reward]] + payment_method_type = "classic" +[[cashtocode.reward]] + payment_method_type = "evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_webhook_details] +merchant_secret="Source verification key" + +[cryptopay] +[[cryptopay.crypto]] + payment_method_type = "crypto_currency" +[cryptopay.connector_auth.BodyKey] +api_key="API Key" +key1="Secret Key" +[cryptopay.connector_webhook_details] +merchant_secret="Source verification key" + +[checkout] +[[checkout.credit]] + payment_method_type = "Mastercard" +[[checkout.credit]] + payment_method_type = "Visa" +[[checkout.credit]] + payment_method_type = "Interac" +[[checkout.credit]] + payment_method_type = "AmericanExpress" +[[checkout.credit]] + payment_method_type = "JCB" +[[checkout.credit]] + payment_method_type = "DinersClub" +[[checkout.credit]] + payment_method_type = "Discover" +[[checkout.credit]] + payment_method_type = "CartesBancaires" +[[checkout.credit]] + payment_method_type = "UnionPay" +[[checkout.debit]] + payment_method_type = "Mastercard" +[[checkout.debit]] + payment_method_type = "Visa" +[[checkout.debit]] + payment_method_type = "Interac" +[[checkout.debit]] + payment_method_type = "AmericanExpress" +[[checkout.debit]] + payment_method_type = "JCB" +[[checkout.debit]] + payment_method_type = "DinersClub" +[[checkout.debit]] + payment_method_type = "Discover" +[[checkout.debit]] + payment_method_type = "CartesBancaires" +[[checkout.debit]] + payment_method_type = "UnionPay" +[[checkout.wallet]] + payment_method_type = "apple_pay" +[[checkout.wallet]] + payment_method_type = "google_pay" +[checkout.connector_auth.SignatureKey] +api_key="Checkout API Public Key" +key1="Processing Channel ID" +api_secret="Checkout API Secret Key" +[checkout.connector_webhook_details] +merchant_secret="Source verification key" + +[checkout.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[checkout.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[checkout.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + + +[coinbase] +[[coinbase.crypto]] + payment_method_type = "crypto_currency" +[coinbase.connector_auth.HeaderKey] +api_key="API Key" +[coinbase.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource] +[[cybersource.credit]] + payment_method_type = "Mastercard" +[[cybersource.credit]] + payment_method_type = "Visa" +[[cybersource.credit]] + payment_method_type = "Interac" +[[cybersource.credit]] + payment_method_type = "AmericanExpress" +[[cybersource.credit]] + payment_method_type = "JCB" +[[cybersource.credit]] + payment_method_type = "DinersClub" +[[cybersource.credit]] + payment_method_type = "Discover" +[[cybersource.credit]] + payment_method_type = "CartesBancaires" +[[cybersource.credit]] + payment_method_type = "UnionPay" +[[cybersource.debit]] + payment_method_type = "Mastercard" +[[cybersource.debit]] + payment_method_type = "Visa" +[[cybersource.debit]] + payment_method_type = "Interac" +[[cybersource.debit]] + payment_method_type = "AmericanExpress" +[[cybersource.debit]] + payment_method_type = "JCB" +[[cybersource.debit]] + payment_method_type = "DinersClub" +[[cybersource.debit]] + payment_method_type = "Discover" +[[cybersource.debit]] + payment_method_type = "CartesBancaires" +[[cybersource.debit]] + payment_method_type = "UnionPay" +[[cybersource.wallet]] + payment_method_type = "apple_pay" +[[cybersource.wallet]] + payment_method_type = "google_pay" +[cybersource.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[cybersource.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[cybersource.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[cybersource.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[dlocal] +[[dlocal.credit]] + payment_method_type = "Mastercard" +[[dlocal.credit]] + payment_method_type = "Visa" +[[dlocal.credit]] + payment_method_type = "Interac" +[[dlocal.credit]] + payment_method_type = "AmericanExpress" +[[dlocal.credit]] + payment_method_type = "JCB" +[[dlocal.credit]] + payment_method_type = "DinersClub" +[[dlocal.credit]] + payment_method_type = "Discover" +[[dlocal.credit]] + payment_method_type = "CartesBancaires" +[[dlocal.credit]] + payment_method_type = "UnionPay" +[[dlocal.debit]] + payment_method_type = "Mastercard" +[[dlocal.debit]] + payment_method_type = "Visa" +[[dlocal.debit]] + payment_method_type = "Interac" +[[dlocal.debit]] + payment_method_type = "AmericanExpress" +[[dlocal.debit]] + payment_method_type = "JCB" +[[dlocal.debit]] + payment_method_type = "DinersClub" +[[dlocal.debit]] + payment_method_type = "Discover" +[[dlocal.debit]] + payment_method_type = "CartesBancaires" +[[dlocal.debit]] + payment_method_type = "UnionPay" +[dlocal.connector_auth.SignatureKey] +api_key="X Login" +key1="X Trans Key" +api_secret="Secret Key" +[dlocal.connector_webhook_details] +merchant_secret="Source verification key" + + +[fiserv] +[[fiserv.credit]] + payment_method_type = "Mastercard" +[[fiserv.credit]] + payment_method_type = "Visa" +[[fiserv.credit]] + payment_method_type = "Interac" +[[fiserv.credit]] + payment_method_type = "AmericanExpress" +[[fiserv.credit]] + payment_method_type = "JCB" +[[fiserv.credit]] + payment_method_type = "DinersClub" +[[fiserv.credit]] + payment_method_type = "Discover" +[[fiserv.credit]] + payment_method_type = "CartesBancaires" +[[fiserv.credit]] + payment_method_type = "UnionPay" +[[fiserv.debit]] + payment_method_type = "Mastercard" +[[fiserv.debit]] + payment_method_type = "Visa" +[[fiserv.debit]] + payment_method_type = "Interac" +[[fiserv.debit]] + payment_method_type = "AmericanExpress" +[[fiserv.debit]] + payment_method_type = "JCB" +[[fiserv.debit]] + payment_method_type = "DinersClub" +[[fiserv.debit]] + payment_method_type = "Discover" +[[fiserv.debit]] + payment_method_type = "CartesBancaires" +[[fiserv.debit]] + payment_method_type = "UnionPay" +[fiserv.connector_auth.SignatureKey] +api_key="API Key" +key1="Merchant ID" +api_secret="API Secret" +[fiserv.connector_webhook_details] +merchant_secret="Source verification key" +[fiserv.metadata] +terminal_id="Terminal ID" + +[forte] +[[forte.credit]] + payment_method_type = "Mastercard" +[[forte.credit]] + payment_method_type = "Visa" +[[forte.credit]] + payment_method_type = "Interac" +[[forte.credit]] + payment_method_type = "AmericanExpress" +[[forte.credit]] + payment_method_type = "JCB" +[[forte.credit]] + payment_method_type = "DinersClub" +[[forte.credit]] + payment_method_type = "Discover" +[[forte.credit]] + payment_method_type = "CartesBancaires" +[[forte.credit]] + payment_method_type = "UnionPay" +[[forte.debit]] + payment_method_type = "Mastercard" +[[forte.debit]] + payment_method_type = "Visa" +[[forte.debit]] + payment_method_type = "Interac" +[[forte.debit]] + payment_method_type = "AmericanExpress" +[[forte.debit]] + payment_method_type = "JCB" +[[forte.debit]] + payment_method_type = "DinersClub" +[[forte.debit]] + payment_method_type = "Discover" +[[forte.debit]] + payment_method_type = "CartesBancaires" +[[forte.debit]] + payment_method_type = "UnionPay" +[forte.connector_auth.MultiAuthKey] +api_key="API Access ID" +key1="Organization ID" +api_secret="API Secure Key" +key2="Location ID" +[forte.connector_webhook_details] +merchant_secret="Source verification key" + + +[globalpay] +[[globalpay.credit]] + payment_method_type = "Mastercard" +[[globalpay.credit]] + payment_method_type = "Visa" +[[globalpay.credit]] + payment_method_type = "Interac" +[[globalpay.credit]] + payment_method_type = "AmericanExpress" +[[globalpay.credit]] + payment_method_type = "JCB" +[[globalpay.credit]] + payment_method_type = "DinersClub" +[[globalpay.credit]] + payment_method_type = "Discover" +[[globalpay.credit]] + payment_method_type = "CartesBancaires" +[[globalpay.credit]] + payment_method_type = "UnionPay" +[[globalpay.debit]] + payment_method_type = "Mastercard" +[[globalpay.debit]] + payment_method_type = "Visa" +[[globalpay.debit]] + payment_method_type = "Interac" +[[globalpay.debit]] + payment_method_type = "AmericanExpress" +[[globalpay.debit]] + payment_method_type = "JCB" +[[globalpay.debit]] + payment_method_type = "DinersClub" +[[globalpay.debit]] + payment_method_type = "Discover" +[[globalpay.debit]] + payment_method_type = "CartesBancaires" +[[globalpay.debit]] + payment_method_type = "UnionPay" +[[globalpay.bank_redirect]] + payment_method_type = "ideal" +[[globalpay.bank_redirect]] + payment_method_type = "giropay" +[[globalpay.bank_redirect]] + payment_method_type = "sofort" +[[globalpay.bank_redirect]] + payment_method_type = "eps" +[[globalpay.wallet]] + payment_method_type = "google_pay" +[[globalpay.wallet]] + payment_method_type = "paypal" +[globalpay.connector_auth.BodyKey] +api_key="Global App Key" +key1="Global App ID" +[globalpay.connector_webhook_details] +merchant_secret="Source verification key" +[globalpay.metadata] +account_name="Account Name" +[globalpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + + +[globepay] +[[globepay.wallet]] + payment_method_type = "we_chat_pay" +[[globepay.wallet]] + payment_method_type = "ali_pay" +[globepay.connector_auth.BodyKey] +api_key="Partner Code" +key1="Credential Code" +[globepay.connector_webhook_details] +merchant_secret="Source verification key" + +[iatapay] +[[iatapay.upi]] + payment_method_type = "upi_collect" +[iatapay.connector_auth.SignatureKey] +api_key="Client ID" +key1="Airline ID" +api_secret="Client Secret" +[iatapay.connector_webhook_details] +merchant_secret="Source verification key" + +[klarna] +[[klarna.pay_later]] + payment_method_type = "klarna" +[klarna.connector_auth.HeaderKey] +api_key="Klarna API Key" +[klarna.connector_webhook_details] +merchant_secret="Source verification key" + + +[mollie] +[[mollie.credit]] + payment_method_type = "Mastercard" +[[mollie.credit]] + payment_method_type = "Visa" +[[mollie.credit]] + payment_method_type = "Interac" +[[mollie.credit]] + payment_method_type = "AmericanExpress" +[[mollie.credit]] + payment_method_type = "JCB" +[[mollie.credit]] + payment_method_type = "DinersClub" +[[mollie.credit]] + payment_method_type = "Discover" +[[mollie.credit]] + payment_method_type = "CartesBancaires" +[[mollie.credit]] + payment_method_type = "UnionPay" +[[mollie.debit]] + payment_method_type = "Mastercard" +[[mollie.debit]] + payment_method_type = "Visa" +[[mollie.debit]] + payment_method_type = "Interac" +[[mollie.debit]] + payment_method_type = "AmericanExpress" +[[mollie.debit]] + payment_method_type = "JCB" +[[mollie.debit]] + payment_method_type = "DinersClub" +[[mollie.debit]] + payment_method_type = "Discover" +[[mollie.debit]] + payment_method_type = "CartesBancaires" +[[mollie.debit]] + payment_method_type = "UnionPay" +[[mollie.bank_redirect]] + payment_method_type = "ideal" +[[mollie.bank_redirect]] + payment_method_type = "giropay" +[[mollie.bank_redirect]] + payment_method_type = "sofort" +[[mollie.bank_redirect]] + payment_method_type = "eps" +[[mollie.wallet]] + payment_method_type = "paypal" +[mollie.connector_auth.BodyKey] +api_key="API Key" +key1="Profile Token" +[mollie.connector_webhook_details] +merchant_secret="Source verification key" + +[multisafepay] +[[multisafepay.credit]] + payment_method_type = "Mastercard" +[[multisafepay.credit]] + payment_method_type = "Visa" +[[multisafepay.credit]] + payment_method_type = "Interac" +[[multisafepay.credit]] + payment_method_type = "AmericanExpress" +[[multisafepay.credit]] + payment_method_type = "JCB" +[[multisafepay.credit]] + payment_method_type = "DinersClub" +[[multisafepay.credit]] + payment_method_type = "Discover" +[[multisafepay.credit]] + payment_method_type = "CartesBancaires" +[[multisafepay.credit]] + payment_method_type = "UnionPay" +[[multisafepay.debit]] + payment_method_type = "Mastercard" +[[multisafepay.debit]] + payment_method_type = "Visa" +[[multisafepay.debit]] + payment_method_type = "Interac" +[[multisafepay.debit]] + payment_method_type = "AmericanExpress" +[[multisafepay.debit]] + payment_method_type = "JCB" +[[multisafepay.debit]] + payment_method_type = "DinersClub" +[[multisafepay.debit]] + payment_method_type = "Discover" +[[multisafepay.debit]] + payment_method_type = "CartesBancaires" +[[multisafepay.debit]] + payment_method_type = "UnionPay" +[multisafepay.connector_auth.HeaderKey] +api_key="Enter API Key" +[multisafepay.connector_webhook_details] +merchant_secret="Source verification key" + + +[nexinets] +[[nexinets.credit]] + payment_method_type = "Mastercard" +[[nexinets.credit]] + payment_method_type = "Visa" +[[nexinets.credit]] + payment_method_type = "Interac" +[[nexinets.credit]] + payment_method_type = "AmericanExpress" +[[nexinets.credit]] + payment_method_type = "JCB" +[[nexinets.credit]] + payment_method_type = "DinersClub" +[[nexinets.credit]] + payment_method_type = "Discover" +[[nexinets.credit]] + payment_method_type = "CartesBancaires" +[[nexinets.credit]] + payment_method_type = "UnionPay" +[[nexinets.debit]] + payment_method_type = "Mastercard" +[[nexinets.debit]] + payment_method_type = "Visa" +[[nexinets.debit]] + payment_method_type = "Interac" +[[nexinets.debit]] + payment_method_type = "AmericanExpress" +[[nexinets.debit]] + payment_method_type = "JCB" +[[nexinets.debit]] + payment_method_type = "DinersClub" +[[nexinets.debit]] + payment_method_type = "Discover" +[[nexinets.debit]] + payment_method_type = "CartesBancaires" +[[nexinets.debit]] + payment_method_type = "UnionPay" +[[nexinets.bank_redirect]] + payment_method_type = "ideal" +[[nexinets.bank_redirect]] + payment_method_type = "giropay" +[[nexinets.bank_redirect]] + payment_method_type = "sofort" +[[nexinets.bank_redirect]] + payment_method_type = "eps" +[[nexinets.wallet]] + payment_method_type = "apple_pay" +[[nexinets.wallet]] + payment_method_type = "paypal" +[nexinets.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant ID" +[nexinets.connector_webhook_details] +merchant_secret="Source verification key" +[nexinets.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nexinets.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[nmi] +[[nmi.credit]] + payment_method_type = "Mastercard" +[[nmi.credit]] + payment_method_type = "Visa" +[[nmi.credit]] + payment_method_type = "Interac" +[[nmi.credit]] + payment_method_type = "AmericanExpress" +[[nmi.credit]] + payment_method_type = "JCB" +[[nmi.credit]] + payment_method_type = "DinersClub" +[[nmi.credit]] + payment_method_type = "Discover" +[[nmi.credit]] + payment_method_type = "CartesBancaires" +[[nmi.credit]] + payment_method_type = "UnionPay" +[[nmi.debit]] + payment_method_type = "Mastercard" +[[nmi.debit]] + payment_method_type = "Visa" +[[nmi.debit]] + payment_method_type = "Interac" +[[nmi.debit]] + payment_method_type = "AmericanExpress" +[[nmi.debit]] + payment_method_type = "JCB" +[[nmi.debit]] + payment_method_type = "DinersClub" +[[nmi.debit]] + payment_method_type = "Discover" +[[nmi.debit]] + payment_method_type = "CartesBancaires" +[[nmi.debit]] + payment_method_type = "UnionPay" +[nmi.connector_auth.BodyKey] +api_key="API Key" +key1="Public Key" +[nmi.connector_webhook_details] +merchant_secret="Source verification key" + +[nuvei] +[[nuvei.credit]] + payment_method_type = "Mastercard" +[[nuvei.credit]] + payment_method_type = "Visa" +[[nuvei.credit]] + payment_method_type = "Interac" +[[nuvei.credit]] + payment_method_type = "AmericanExpress" +[[nuvei.credit]] + payment_method_type = "JCB" +[[nuvei.credit]] + payment_method_type = "DinersClub" +[[nuvei.credit]] + payment_method_type = "Discover" +[[nuvei.credit]] + payment_method_type = "CartesBancaires" +[[nuvei.credit]] + payment_method_type = "UnionPay" +[[nuvei.debit]] + payment_method_type = "Mastercard" +[[nuvei.debit]] + payment_method_type = "Visa" +[[nuvei.debit]] + payment_method_type = "Interac" +[[nuvei.debit]] + payment_method_type = "AmericanExpress" +[[nuvei.debit]] + payment_method_type = "JCB" +[[nuvei.debit]] + payment_method_type = "DinersClub" +[[nuvei.debit]] + payment_method_type = "Discover" +[[nuvei.debit]] + payment_method_type = "CartesBancaires" +[[nuvei.debit]] + payment_method_type = "UnionPay" +[nuvei.connector_auth.SignatureKey] +api_key="Merchant ID" +key1="Merchant Site ID" +api_secret="Merchant Secret" +[nuvei.connector_webhook_details] +merchant_secret="Source verification key" + + + + +[opennode] +[[opennode.crypto]] + payment_method_type = "crypto_currency" +[opennode.connector_auth.HeaderKey] +api_key="API Key" +[opennode.connector_webhook_details] +merchant_secret="Source verification key" + +[paypal] +[[paypal.credit]] + payment_method_type = "Mastercard" +[[paypal.credit]] + payment_method_type = "Visa" +[[paypal.credit]] + payment_method_type = "Interac" +[[paypal.credit]] + payment_method_type = "AmericanExpress" +[[paypal.credit]] + payment_method_type = "JCB" +[[paypal.credit]] + payment_method_type = "DinersClub" +[[paypal.credit]] + payment_method_type = "Discover" +[[paypal.credit]] + payment_method_type = "CartesBancaires" +[[paypal.credit]] + payment_method_type = "UnionPay" +[[paypal.debit]] + payment_method_type = "Mastercard" +[[paypal.debit]] + payment_method_type = "Visa" +[[paypal.debit]] + payment_method_type = "Interac" +[[paypal.debit]] + payment_method_type = "AmericanExpress" +[[paypal.debit]] + payment_method_type = "JCB" +[[paypal.debit]] + payment_method_type = "DinersClub" +[[paypal.debit]] + payment_method_type = "Discover" +[[paypal.debit]] + payment_method_type = "CartesBancaires" +[[paypal.debit]] + payment_method_type = "UnionPay" +[[paypal.wallet]] + payment_method_type = "paypal" +is_verifiable = true +[paypal.connector_auth.BodyKey] +api_key="Client Secret" +key1="Client ID" +[paypal.connector_webhook_details] +merchant_secret="Source verification key" + +[payu] +[[payu.credit]] + payment_method_type = "Mastercard" +[[payu.credit]] + payment_method_type = "Visa" +[[payu.credit]] + payment_method_type = "Interac" +[[payu.credit]] + payment_method_type = "AmericanExpress" +[[payu.credit]] + payment_method_type = "JCB" +[[payu.credit]] + payment_method_type = "DinersClub" +[[payu.credit]] + payment_method_type = "Discover" +[[payu.credit]] + payment_method_type = "CartesBancaires" +[[payu.credit]] + payment_method_type = "UnionPay" +[[payu.debit]] + payment_method_type = "Mastercard" +[[payu.debit]] + payment_method_type = "Visa" +[[payu.debit]] + payment_method_type = "Interac" +[[payu.debit]] + payment_method_type = "AmericanExpress" +[[payu.debit]] + payment_method_type = "JCB" +[[payu.debit]] + payment_method_type = "DinersClub" +[[payu.debit]] + payment_method_type = "Discover" +[[payu.debit]] + payment_method_type = "CartesBancaires" +[[payu.debit]] + payment_method_type = "UnionPay" +[[payu.wallet]] + payment_method_type = "google_pay" +[payu.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant POS ID" +[payu.connector_webhook_details] +merchant_secret="Source verification key" + +[payu.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[rapyd] +[[rapyd.credit]] + payment_method_type = "Mastercard" +[[rapyd.credit]] + payment_method_type = "Visa" +[[rapyd.credit]] + payment_method_type = "Interac" +[[rapyd.credit]] + payment_method_type = "AmericanExpress" +[[rapyd.credit]] + payment_method_type = "JCB" +[[rapyd.credit]] + payment_method_type = "DinersClub" +[[rapyd.credit]] + payment_method_type = "Discover" +[[rapyd.credit]] + payment_method_type = "CartesBancaires" +[[rapyd.credit]] + payment_method_type = "UnionPay" +[[rapyd.debit]] + payment_method_type = "Mastercard" +[[rapyd.debit]] + payment_method_type = "Visa" +[[rapyd.debit]] + payment_method_type = "Interac" +[[rapyd.debit]] + payment_method_type = "AmericanExpress" +[[rapyd.debit]] + payment_method_type = "JCB" +[[rapyd.debit]] + payment_method_type = "DinersClub" +[[rapyd.debit]] + payment_method_type = "Discover" +[[rapyd.debit]] + payment_method_type = "CartesBancaires" +[[rapyd.debit]] + payment_method_type = "UnionPay" +[[rapyd.wallet]] + payment_method_type = "apple_pay" +[rapyd.connector_auth.BodyKey] +api_key="Access Key" +key1="API Secret" +[rapyd.connector_webhook_details] +merchant_secret="Source verification key" + +[rapyd.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[rapyd.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[shift4] +[[shift4.credit]] + payment_method_type = "Mastercard" +[[shift4.credit]] + payment_method_type = "Visa" +[[shift4.credit]] + payment_method_type = "Interac" +[[shift4.credit]] + payment_method_type = "AmericanExpress" +[[shift4.credit]] + payment_method_type = "JCB" +[[shift4.credit]] + payment_method_type = "DinersClub" +[[shift4.credit]] + payment_method_type = "Discover" +[[shift4.credit]] + payment_method_type = "CartesBancaires" +[[shift4.credit]] + payment_method_type = "UnionPay" +[[shift4.debit]] + payment_method_type = "Mastercard" +[[shift4.debit]] + payment_method_type = "Visa" +[[shift4.debit]] + payment_method_type = "Interac" +[[shift4.debit]] + payment_method_type = "AmericanExpress" +[[shift4.debit]] + payment_method_type = "JCB" +[[shift4.debit]] + payment_method_type = "DinersClub" +[[shift4.debit]] + payment_method_type = "Discover" +[[shift4.debit]] + payment_method_type = "CartesBancaires" +[[shift4.debit]] + payment_method_type = "UnionPay" +[[shift4.bank_redirect]] + payment_method_type = "ideal" +[[shift4.bank_redirect]] + payment_method_type = "giropay" +[[shift4.bank_redirect]] + payment_method_type = "sofort" +[[shift4.bank_redirect]] + payment_method_type = "eps" +[shift4.connector_auth.HeaderKey] +api_key="API Key" +[shift4.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe] +[[stripe.credit]] + payment_method_type = "Mastercard" +[[stripe.credit]] + payment_method_type = "Visa" +[[stripe.credit]] + payment_method_type = "Interac" +[[stripe.credit]] + payment_method_type = "AmericanExpress" +[[stripe.credit]] + payment_method_type = "JCB" +[[stripe.credit]] + payment_method_type = "DinersClub" +[[stripe.credit]] + payment_method_type = "Discover" +[[stripe.credit]] + payment_method_type = "CartesBancaires" +[[stripe.credit]] + payment_method_type = "UnionPay" +[[stripe.debit]] + payment_method_type = "Mastercard" +[[stripe.debit]] + payment_method_type = "Visa" +[[stripe.debit]] + payment_method_type = "Interac" +[[stripe.debit]] + payment_method_type = "AmericanExpress" +[[stripe.debit]] + payment_method_type = "JCB" +[[stripe.debit]] + payment_method_type = "DinersClub" +[[stripe.debit]] + payment_method_type = "Discover" +[[stripe.debit]] + payment_method_type = "CartesBancaires" +[[stripe.debit]] + payment_method_type = "UnionPay" +[[stripe.pay_later]] + payment_method_type = "klarna" +[[stripe.pay_later]] + payment_method_type = "affirm" +[[stripe.pay_later]] + payment_method_type = "afterpay_clearpay" +[[stripe.bank_redirect]] + payment_method_type = "ideal" +[[stripe.bank_redirect]] + payment_method_type = "giropay" +[[stripe.bank_redirect]] + payment_method_type = "sofort" +[[stripe.bank_redirect]] + payment_method_type = "eps" +[[stripe.bank_debit]] + payment_method_type = "ach" +[[stripe.bank_debit]] + payment_method_type = "becs" +[[stripe.bank_debit]] + payment_method_type = "sepa" +[[stripe.bank_transfer]] + payment_method_type = "ach" +[[stripe.bank_transfer]] + payment_method_type = "bacs" +[[stripe.bank_transfer]] + payment_method_type = "sepa" +[[stripe.wallet]] + payment_method_type = "apple_pay" +[[stripe.wallet]] + payment_method_type = "google_pay" +is_verifiable = true +[stripe.connector_auth.HeaderKey] +api_key="Secret Key" +[stripe.connector_webhook_details] +merchant_secret="Source verification key" +[stripe.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +stripe_publishable_key="Stripe Publishable Key" +merchant_id="Google Pay Merchant ID" +[stripe.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[stripe.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + + + +[trustpay] +[[trustpay.credit]] + payment_method_type = "Mastercard" +[[trustpay.credit]] + payment_method_type = "Visa" +[[trustpay.credit]] + payment_method_type = "Interac" +[[trustpay.credit]] + payment_method_type = "AmericanExpress" +[[trustpay.credit]] + payment_method_type = "JCB" +[[trustpay.credit]] + payment_method_type = "DinersClub" +[[trustpay.credit]] + payment_method_type = "Discover" +[[trustpay.credit]] + payment_method_type = "CartesBancaires" +[[trustpay.credit]] + payment_method_type = "UnionPay" +[[trustpay.debit]] + payment_method_type = "Mastercard" +[[trustpay.debit]] + payment_method_type = "Visa" +[[trustpay.debit]] + payment_method_type = "Interac" +[[trustpay.debit]] + payment_method_type = "AmericanExpress" +[[trustpay.debit]] + payment_method_type = "JCB" +[[trustpay.debit]] + payment_method_type = "DinersClub" +[[trustpay.debit]] + payment_method_type = "Discover" +[[trustpay.debit]] + payment_method_type = "CartesBancaires" +[[trustpay.debit]] + payment_method_type = "UnionPay" +[[trustpay.bank_redirect]] + payment_method_type = "ideal" +[[trustpay.bank_redirect]] + payment_method_type = "giropay" +[[trustpay.bank_redirect]] + payment_method_type = "sofort" +[[trustpay.bank_redirect]] + payment_method_type = "eps" +[[trustpay.bank_redirect]] + payment_method_type = "blik" +[[trustpay.wallet]] + payment_method_type = "apple_pay" +[[trustpay.wallet]] + payment_method_type = "google_pay" +[trustpay.connector_auth.SignatureKey] +api_key="API Key" +key1="Project ID" +api_secret="Secret Key" +[trustpay.connector_webhook_details] +merchant_secret="Source verification key" +[trustpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[trustpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[worldline] +[[worldline.credit]] + payment_method_type = "Mastercard" +[[worldline.credit]] + payment_method_type = "Visa" +[[worldline.credit]] + payment_method_type = "Interac" +[[worldline.credit]] + payment_method_type = "AmericanExpress" +[[worldline.credit]] + payment_method_type = "JCB" +[[worldline.credit]] + payment_method_type = "DinersClub" +[[worldline.credit]] + payment_method_type = "Discover" +[[worldline.credit]] + payment_method_type = "CartesBancaires" +[[worldline.credit]] + payment_method_type = "UnionPay" +[[worldline.debit]] + payment_method_type = "Mastercard" +[[worldline.debit]] + payment_method_type = "Visa" +[[worldline.debit]] + payment_method_type = "Interac" +[[worldline.debit]] + payment_method_type = "AmericanExpress" +[[worldline.debit]] + payment_method_type = "JCB" +[[worldline.debit]] + payment_method_type = "DinersClub" +[[worldline.debit]] + payment_method_type = "Discover" +[[worldline.debit]] + payment_method_type = "CartesBancaires" +[[worldline.debit]] + payment_method_type = "UnionPay" +[[worldline.bank_redirect]] + payment_method_type = "ideal" +[[worldline.bank_redirect]] + payment_method_type = "giropay" +[worldline.connector_auth.SignatureKey] +api_key="API Key ID" +key1="Merchant ID" +api_secret="Secret API Key" +[worldline.connector_webhook_details] +merchant_secret="Source verification key" + +[worldpay] +[[worldpay.credit]] + payment_method_type = "Mastercard" +[[worldpay.credit]] + payment_method_type = "Visa" +[[worldpay.credit]] + payment_method_type = "Interac" +[[worldpay.credit]] + payment_method_type = "AmericanExpress" +[[worldpay.credit]] + payment_method_type = "JCB" +[[worldpay.credit]] + payment_method_type = "DinersClub" +[[worldpay.credit]] + payment_method_type = "Discover" +[[worldpay.credit]] + payment_method_type = "CartesBancaires" +[[worldpay.credit]] + payment_method_type = "UnionPay" +[[worldpay.debit]] + payment_method_type = "Mastercard" +[[worldpay.debit]] + payment_method_type = "Visa" +[[worldpay.debit]] + payment_method_type = "Interac" +[[worldpay.debit]] + payment_method_type = "AmericanExpress" +[[worldpay.debit]] + payment_method_type = "JCB" +[[worldpay.debit]] + payment_method_type = "DinersClub" +[[worldpay.debit]] + payment_method_type = "Discover" +[[worldpay.debit]] + payment_method_type = "CartesBancaires" +[[worldpay.debit]] + payment_method_type = "UnionPay" +[[worldpay.wallet]] + payment_method_type = "google_pay" +[[worldpay.wallet]] + payment_method_type = "apple_pay" +[worldpay.connector_auth.BodyKey] +api_key="Username" +key1="Password" +[worldpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" +[worldpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[worldpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" +[worldpay.connector_webhook_details] +merchant_secret="Source verification key" + + + +[payme] +[[payme.credit]] + payment_method_type = "Mastercard" +[[payme.credit]] + payment_method_type = "Visa" +[[payme.credit]] + payment_method_type = "Interac" +[[payme.credit]] + payment_method_type = "AmericanExpress" +[[payme.credit]] + payment_method_type = "JCB" +[[payme.credit]] + payment_method_type = "DinersClub" +[[payme.credit]] + payment_method_type = "Discover" +[[payme.credit]] + payment_method_type = "CartesBancaires" +[[payme.credit]] + payment_method_type = "UnionPay" +[[payme.debit]] + payment_method_type = "Mastercard" +[[payme.debit]] + payment_method_type = "Visa" +[[payme.debit]] + payment_method_type = "Interac" +[[payme.debit]] + payment_method_type = "AmericanExpress" +[[payme.debit]] + payment_method_type = "JCB" +[[payme.debit]] + payment_method_type = "DinersClub" +[[payme.debit]] + payment_method_type = "Discover" +[[payme.debit]] + payment_method_type = "CartesBancaires" +[[payme.debit]] + payment_method_type = "UnionPay" +[payme.connector_auth.BodyKey] +api_key="Seller Payme Id" +key1="Payme Public Key" +[payme.connector_webhook_details] +merchant_secret="Payme Client Secret" +additional_secret="Payme Client Key" + +[powertranz] +[[powertranz.credit]] + payment_method_type = "Mastercard" +[[powertranz.credit]] + payment_method_type = "Visa" +[[powertranz.credit]] + payment_method_type = "Interac" +[[powertranz.credit]] + payment_method_type = "AmericanExpress" +[[powertranz.credit]] + payment_method_type = "JCB" +[[powertranz.credit]] + payment_method_type = "DinersClub" +[[powertranz.credit]] + payment_method_type = "Discover" +[[powertranz.credit]] + payment_method_type = "CartesBancaires" +[[powertranz.credit]] + payment_method_type = "UnionPay" +[[powertranz.debit]] + payment_method_type = "Mastercard" +[[powertranz.debit]] + payment_method_type = "Visa" +[[powertranz.debit]] + payment_method_type = "Interac" +[[powertranz.debit]] + payment_method_type = "AmericanExpress" +[[powertranz.debit]] + payment_method_type = "JCB" +[[powertranz.debit]] + payment_method_type = "DinersClub" +[[powertranz.debit]] + payment_method_type = "Discover" +[[powertranz.debit]] + payment_method_type = "CartesBancaires" +[[powertranz.debit]] + payment_method_type = "UnionPay" +[powertranz.connector_auth.BodyKey] +key1 = "PowerTranz Id" +api_key="PowerTranz Password" +[powertranz.connector_webhook_details] +merchant_secret="Source verification key" + + + +[tsys] +[[tsys.credit]] + payment_method_type = "Mastercard" +[[tsys.credit]] + payment_method_type = "Visa" +[[tsys.credit]] + payment_method_type = "Interac" +[[tsys.credit]] + payment_method_type = "AmericanExpress" +[[tsys.credit]] + payment_method_type = "JCB" +[[tsys.credit]] + payment_method_type = "DinersClub" +[[tsys.credit]] + payment_method_type = "Discover" +[[tsys.credit]] + payment_method_type = "CartesBancaires" +[[tsys.credit]] + payment_method_type = "UnionPay" +[[tsys.debit]] + payment_method_type = "Mastercard" +[[tsys.debit]] + payment_method_type = "Visa" +[[tsys.debit]] + payment_method_type = "Interac" +[[tsys.debit]] + payment_method_type = "AmericanExpress" +[[tsys.debit]] + payment_method_type = "JCB" +[[tsys.debit]] + payment_method_type = "DinersClub" +[[tsys.debit]] + payment_method_type = "Discover" +[[tsys.debit]] + payment_method_type = "CartesBancaires" +[[tsys.debit]] + payment_method_type = "UnionPay" +[tsys.connector_auth.SignatureKey] +api_key="Device Id" +key1="Transaction Key" +api_secret="Developer Id" +[tsys.connector_webhook_details] +merchant_secret="Source verification key" + +[zen] +[[zen.credit]] + payment_method_type = "Mastercard" +[[zen.credit]] + payment_method_type = "Visa" +[[zen.credit]] + payment_method_type = "Interac" +[[zen.credit]] + payment_method_type = "AmericanExpress" +[[zen.credit]] + payment_method_type = "JCB" +[[zen.credit]] + payment_method_type = "DinersClub" +[[zen.credit]] + payment_method_type = "Discover" +[[zen.credit]] + payment_method_type = "CartesBancaires" +[[zen.credit]] + payment_method_type = "UnionPay" +[[zen.debit]] + payment_method_type = "Mastercard" +[[zen.debit]] + payment_method_type = "Visa" +[[zen.debit]] + payment_method_type = "Interac" +[[zen.debit]] + payment_method_type = "AmericanExpress" +[[zen.debit]] + payment_method_type = "JCB" +[[zen.debit]] + payment_method_type = "DinersClub" +[[zen.debit]] + payment_method_type = "Discover" +[[zen.debit]] + payment_method_type = "CartesBancaires" +[[zen.debit]] + payment_method_type = "UnionPay" +[[zen.wallet]] + payment_method_type = "apple_pay" +[[zen.wallet]] + payment_method_type = "google_pay" +[[zen.voucher]] + payment_method_type = "boleto" +[[zen.voucher]] + payment_method_type = "efecty" +[[zen.voucher]] + payment_method_type = "pago_efectivo" +[[zen.voucher]] + payment_method_type = "red_compra" +[[zen.voucher]] + payment_method_type = "red_pagos" +[[zen.bank_transfer]] + payment_method_type = "pix" +[[zen.bank_transfer]] + payment_method_type = "pse" +[zen.connector_auth.HeaderKey] +api_key="API Key" +[zen.connector_webhook_details] +merchant_secret="Source verification key" +[zen.metadata.apple_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" +[zen.metadata.google_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" \ No newline at end of file diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml new file mode 100644 index 000000000000..576cd47c6eec --- /dev/null +++ b/crates/connector_configs/toml/sandbox.toml @@ -0,0 +1,2523 @@ +[aci] +[[aci.credit]] + payment_method_type = "Mastercard" +[[aci.credit]] + payment_method_type = "Visa" +[[aci.credit]] + payment_method_type = "Interac" +[[aci.credit]] + payment_method_type = "AmericanExpress" +[[aci.credit]] + payment_method_type = "JCB" +[[aci.credit]] + payment_method_type = "DinersClub" +[[aci.credit]] + payment_method_type = "Discover" +[[aci.credit]] + payment_method_type = "CartesBancaires" +[[aci.credit]] + payment_method_type = "UnionPay" +[[aci.debit]] + payment_method_type = "Mastercard" +[[aci.debit]] + payment_method_type = "Visa" +[[aci.debit]] + payment_method_type = "Interac" +[[aci.debit]] + payment_method_type = "AmericanExpress" +[[aci.debit]] + payment_method_type = "JCB" +[[aci.debit]] + payment_method_type = "DinersClub" +[[aci.debit]] + payment_method_type = "Discover" +[[aci.debit]] + payment_method_type = "CartesBancaires" +[[aci.debit]] + payment_method_type = "UnionPay" +[[aci.wallet]] + payment_method_type = "ali_pay" +[[aci.wallet]] + payment_method_type = "mb_way" +[[aci.bank_redirect]] + payment_method_type = "ideal" +[[aci.bank_redirect]] + payment_method_type = "giropay" +[[aci.bank_redirect]] + payment_method_type = "sofort" +[[aci.bank_redirect]] + payment_method_type = "eps" +[[aci.bank_redirect]] + payment_method_type = "przelewy24" +[[aci.bank_redirect]] + payment_method_type = "trustly" +[[aci.bank_redirect]] + payment_method_type = "interac" +[aci.connector_auth.BodyKey] +api_key="API Key" +key1="Entity ID" +[aci.connector_webhook_details] +merchant_secret="Source verification key" + + +[adyen] +[[adyen.credit]] + payment_method_type = "Mastercard" +[[adyen.credit]] + payment_method_type = "Visa" +[[adyen.credit]] + payment_method_type = "Interac" +[[adyen.credit]] + payment_method_type = "AmericanExpress" +[[adyen.credit]] + payment_method_type = "JCB" +[[adyen.credit]] + payment_method_type = "DinersClub" +[[adyen.credit]] + payment_method_type = "Discover" +[[adyen.credit]] + payment_method_type = "CartesBancaires" +[[adyen.credit]] + payment_method_type = "UnionPay" +[[adyen.debit]] + payment_method_type = "Mastercard" +[[adyen.debit]] + payment_method_type = "Visa" +[[adyen.debit]] + payment_method_type = "Interac" +[[adyen.debit]] + payment_method_type = "AmericanExpress" +[[adyen.debit]] + payment_method_type = "JCB" +[[adyen.debit]] + payment_method_type = "DinersClub" +[[adyen.debit]] + payment_method_type = "Discover" +[[adyen.debit]] + payment_method_type = "CartesBancaires" +[[adyen.debit]] + payment_method_type = "UnionPay" +[[adyen.pay_later]] + payment_method_type = "klarna" +[[adyen.pay_later]] + payment_method_type = "affirm" +[[adyen.pay_later]] + payment_method_type = "afterpay_clearpay" +[[adyen.pay_later]] + payment_method_type = "pay_bright" +[[adyen.pay_later]] + payment_method_type = "walley" +[[adyen.pay_later]] + payment_method_type = "alma" +[[adyen.pay_later]] + payment_method_type = "atome" +[[adyen.bank_debit]] + payment_method_type = "ach" +[[adyen.bank_debit]] + payment_method_type = "bacs" +[[adyen.bank_debit]] + payment_method_type = "sepa" +[[adyen.bank_redirect]] + payment_method_type = "ideal" +[[adyen.bank_redirect]] + payment_method_type = "giropay" +[[adyen.bank_redirect]] + payment_method_type = "sofort" +[[adyen.bank_redirect]] + payment_method_type = "eps" +[[adyen.bank_redirect]] + payment_method_type = "blik" +[[adyen.bank_redirect]] + payment_method_type = "przelewy24" +[[adyen.bank_redirect]] + payment_method_type = "trustly" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_czech_republic" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_finland" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_poland" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_slovakia" +[[adyen.bank_redirect]] + payment_method_type = "bancontact_card" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_fpx" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_thailand" +[[adyen.bank_redirect]] + payment_method_type = "bizum" +[[adyen.bank_redirect]] + payment_method_type = "open_banking_uk" +[[adyen.bank_transfer]] + payment_method_type = "permata_bank_transfer" +[[adyen.bank_transfer]] + payment_method_type = "bca_bank_transfer" +[[adyen.bank_transfer]] + payment_method_type = "bni_va" +[[adyen.bank_transfer]] + payment_method_type = "bri_va" +[[adyen.bank_transfer]] + payment_method_type = "cimb_va" +[[adyen.bank_transfer]] + payment_method_type = "danamon_va" +[[adyen.bank_transfer]] + payment_method_type = "mandiri_va" +[[adyen.bank_transfer]] + payment_method_type = "pix" +[[adyen.wallet]] + payment_method_type = "apple_pay" +[[adyen.wallet]] + payment_method_type = "google_pay" +[[adyen.wallet]] + payment_method_type = "paypal" +[[adyen.wallet]] + payment_method_type = "we_chat_pay" +[[adyen.wallet]] + payment_method_type = "ali_pay" +[[adyen.wallet]] + payment_method_type = "mb_way" +[[adyen.wallet]] + payment_method_type = "ali_pay_hk" +[[adyen.wallet]] + payment_method_type = "go_pay" +[[adyen.wallet]] + payment_method_type = "kakao_pay" +[[adyen.wallet]] + payment_method_type = "twint" +[[adyen.wallet]] + payment_method_type = "gcash" +[[adyen.wallet]] + payment_method_type = "vipps" +[[adyen.wallet]] + payment_method_type = "dana" +[[adyen.wallet]] + payment_method_type = "momo" +[[adyen.wallet]] + payment_method_type = "swish" +[[adyen.wallet]] + payment_method_type = "touch_n_go" +[[adyen.voucher]] + payment_method_type = "boleto" +[[adyen.voucher]] + payment_method_type = "alfamart" +[[adyen.voucher]] + payment_method_type = "indomaret" +[[adyen.voucher]] + payment_method_type = "oxxo" +[[adyen.voucher]] + payment_method_type = "seven_eleven" +[[adyen.voucher]] + payment_method_type = "lawson" +[[adyen.voucher]] + payment_method_type = "mini_stop" +[[adyen.voucher]] + payment_method_type = "family_mart" +[[adyen.voucher]] + payment_method_type = "seicomart" +[[adyen.voucher]] + payment_method_type = "pay_easy" +[[adyen.gift_card]] + payment_method_type = "pay_safe_card" +[[adyen.gift_card]] + payment_method_type = "givex" +[[adyen.card_redirect]] + payment_method_type = "benefit" +[[adyen.card_redirect]] + payment_method_type = "knet" +[[adyen.card_redirect]] + payment_method_type = "momo_atm" +[adyen.connector_auth.BodyKey] +api_key="Adyen API Key" +key1="Adyen Account Id" +[adyen.connector_webhook_details] +merchant_secret="Source verification key" +[adyen.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[adyen.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[adyen.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[airwallex] +[[airwallex.credit]] + payment_method_type = "Mastercard" +[[airwallex.credit]] + payment_method_type = "Visa" +[[airwallex.credit]] + payment_method_type = "Interac" +[[airwallex.credit]] + payment_method_type = "AmericanExpress" +[[airwallex.credit]] + payment_method_type = "JCB" +[[airwallex.credit]] + payment_method_type = "DinersClub" +[[airwallex.credit]] + payment_method_type = "Discover" +[[airwallex.credit]] + payment_method_type = "CartesBancaires" +[[airwallex.credit]] + payment_method_type = "UnionPay" +[[airwallex.debit]] + payment_method_type = "Mastercard" +[[airwallex.debit]] + payment_method_type = "Visa" +[[airwallex.debit]] + payment_method_type = "Interac" +[[airwallex.debit]] + payment_method_type = "AmericanExpress" +[[airwallex.debit]] + payment_method_type = "JCB" +[[airwallex.debit]] + payment_method_type = "DinersClub" +[[airwallex.debit]] + payment_method_type = "Discover" +[[airwallex.debit]] + payment_method_type = "CartesBancaires" +[[airwallex.debit]] + payment_method_type = "UnionPay" +[[airwallex.wallet]] + payment_method_type = "google_pay" +[airwallex.connector_auth.BodyKey] +api_key="API Key" +key1="Client ID" +[airwallex.connector_webhook_details] +merchant_secret="Source verification key" + + +[authorizedotnet] +[[authorizedotnet.credit]] + payment_method_type = "Mastercard" +[[authorizedotnet.credit]] + payment_method_type = "Visa" +[[authorizedotnet.credit]] + payment_method_type = "Interac" +[[authorizedotnet.credit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.credit]] + payment_method_type = "JCB" +[[authorizedotnet.credit]] + payment_method_type = "DinersClub" +[[authorizedotnet.credit]] + payment_method_type = "Discover" +[[authorizedotnet.credit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.credit]] + payment_method_type = "UnionPay" +[[authorizedotnet.debit]] + payment_method_type = "Mastercard" +[[authorizedotnet.debit]] + payment_method_type = "Visa" +[[authorizedotnet.debit]] + payment_method_type = "Interac" +[[authorizedotnet.debit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.debit]] + payment_method_type = "JCB" +[[authorizedotnet.debit]] + payment_method_type = "DinersClub" +[[authorizedotnet.debit]] + payment_method_type = "Discover" +[[authorizedotnet.debit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.debit]] + payment_method_type = "UnionPay" +[[authorizedotnet.wallet]] + payment_method_type = "google_pay" +[[authorizedotnet.wallet]] + payment_method_type = "paypal" +[authorizedotnet.connector_auth.BodyKey] +api_key="API Login ID" +key1="Transaction Key" +[authorizedotnet.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" +[authorizedotnet.connector_webhook_details] +merchant_secret="Source verification key" + +[bambora] +[[bambora.credit]] + payment_method_type = "Mastercard" +[[bambora.credit]] + payment_method_type = "Visa" +[[bambora.credit]] + payment_method_type = "Interac" +[[bambora.credit]] + payment_method_type = "AmericanExpress" +[[bambora.credit]] + payment_method_type = "JCB" +[[bambora.credit]] + payment_method_type = "DinersClub" +[[bambora.credit]] + payment_method_type = "Discover" +[[bambora.credit]] + payment_method_type = "CartesBancaires" +[[bambora.credit]] + payment_method_type = "UnionPay" +[[bambora.debit]] + payment_method_type = "Mastercard" +[[bambora.debit]] + payment_method_type = "Visa" +[[bambora.debit]] + payment_method_type = "Interac" +[[bambora.debit]] + payment_method_type = "AmericanExpress" +[[bambora.debit]] + payment_method_type = "JCB" +[[bambora.debit]] + payment_method_type = "DinersClub" +[[bambora.debit]] + payment_method_type = "Discover" +[[bambora.debit]] + payment_method_type = "CartesBancaires" +[[bambora.debit]] + payment_method_type = "UnionPay" +[[bambora.wallet]] + payment_method_type = "apple_pay" +[[bambora.wallet]] + payment_method_type = "paypal" +[bambora.connector_auth.BodyKey] +api_key="Passcode" +key1="Merchant Id" +[bambora.connector_webhook_details] +merchant_secret="Source verification key" +[bambora.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bambora.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica] +[[bankofamerica.credit]] + payment_method_type = "Mastercard" +[[bankofamerica.credit]] + payment_method_type = "Visa" +[[bankofamerica.credit]] + payment_method_type = "Interac" +[[bankofamerica.credit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.credit]] + payment_method_type = "JCB" +[[bankofamerica.credit]] + payment_method_type = "DinersClub" +[[bankofamerica.credit]] + payment_method_type = "Discover" +[[bankofamerica.credit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.credit]] + payment_method_type = "UnionPay" +[[bankofamerica.debit]] + payment_method_type = "Mastercard" +[[bankofamerica.debit]] + payment_method_type = "Visa" +[[bankofamerica.debit]] + payment_method_type = "Interac" +[[bankofamerica.debit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.debit]] + payment_method_type = "JCB" +[[bankofamerica.debit]] + payment_method_type = "DinersClub" +[[bankofamerica.debit]] + payment_method_type = "Discover" +[[bankofamerica.debit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.debit]] + payment_method_type = "UnionPay" +[[bankofamerica.wallet]] + payment_method_type = "apple_pay" +[[bankofamerica.wallet]] + payment_method_type = "google_pay" +[bankofamerica.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[bankofamerica.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bankofamerica.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[bitpay] +[[bitpay.crypto]] + payment_method_type = "crypto_currency" +[bitpay.connector_auth.HeaderKey] +api_key="API Key" +[bitpay.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap] +[[bluesnap.credit]] + payment_method_type = "Mastercard" +[[bluesnap.credit]] + payment_method_type = "Visa" +[[bluesnap.credit]] + payment_method_type = "Interac" +[[bluesnap.credit]] + payment_method_type = "AmericanExpress" +[[bluesnap.credit]] + payment_method_type = "JCB" +[[bluesnap.credit]] + payment_method_type = "DinersClub" +[[bluesnap.credit]] + payment_method_type = "Discover" +[[bluesnap.credit]] + payment_method_type = "CartesBancaires" +[[bluesnap.credit]] + payment_method_type = "UnionPay" +[[bluesnap.debit]] + payment_method_type = "Mastercard" +[[bluesnap.debit]] + payment_method_type = "Visa" +[[bluesnap.debit]] + payment_method_type = "Interac" +[[bluesnap.debit]] + payment_method_type = "AmericanExpress" +[[bluesnap.debit]] + payment_method_type = "JCB" +[[bluesnap.debit]] + payment_method_type = "DinersClub" +[[bluesnap.debit]] + payment_method_type = "Discover" +[[bluesnap.debit]] + payment_method_type = "CartesBancaires" +[[bluesnap.debit]] + payment_method_type = "UnionPay" +[[bluesnap.wallet]] + payment_method_type = "google_pay" +[[bluesnap.wallet]] + payment_method_type = "apple_pay" +[bluesnap.connector_auth.BodyKey] +api_key="Password" +key1="Username" +[bluesnap.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[bluesnap.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bluesnap.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" +[bluesnap.metadata] +merchant_id="Merchant Id" + +[boku] +[[boku.wallet]] + payment_method_type = "dana" +[[boku.wallet]] + payment_method_type = "gcash" +[[boku.wallet]] + payment_method_type = "go_pay" +[[boku.wallet]] + payment_method_type = "kakao_pay" +[[boku.wallet]] + payment_method_type = "momo" +[boku.connector_auth.BodyKey] +api_key="API KEY" +key1= "MERCHANT ID" +[boku.connector_webhook_details] +merchant_secret="Source verification key" + + +[braintree] +[[braintree.credit]] + payment_method_type = "Mastercard" +[[braintree.credit]] + payment_method_type = "Visa" +[[braintree.credit]] + payment_method_type = "Interac" +[[braintree.credit]] + payment_method_type = "AmericanExpress" +[[braintree.credit]] + payment_method_type = "JCB" +[[braintree.credit]] + payment_method_type = "DinersClub" +[[braintree.credit]] + payment_method_type = "Discover" +[[braintree.credit]] + payment_method_type = "CartesBancaires" +[[braintree.credit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "Mastercard" +[[braintree.debit]] + payment_method_type = "Visa" +[[braintree.debit]] + payment_method_type = "Interac" +[[braintree.debit]] + payment_method_type = "AmericanExpress" +[[braintree.debit]] + payment_method_type = "JCB" +[[braintree.debit]] + payment_method_type = "DinersClub" +[[braintree.debit]] + payment_method_type = "Discover" +[[braintree.debit]] + payment_method_type = "CartesBancaires" +[[braintree.debit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "UnionPay" +[braintree.connector_webhook_details] +merchant_secret="Source verification key" + + +[braintree.connector_auth.SignatureKey] +api_key="Public Key" +key1="Merchant Id" +api_secret="Private Key" +[braintree.metadata] +merchant_account_id="Merchant Account Id" +merchant_config_currency="Currency" + +[cashtocode] +[[cashtocode.reward]] + payment_method_type = "classic" +[[cashtocode.reward]] + payment_method_type = "evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_webhook_details] +merchant_secret="Source verification key" + +[checkout] +[[checkout.credit]] + payment_method_type = "Mastercard" +[[checkout.credit]] + payment_method_type = "Visa" +[[checkout.credit]] + payment_method_type = "Interac" +[[checkout.credit]] + payment_method_type = "AmericanExpress" +[[checkout.credit]] + payment_method_type = "JCB" +[[checkout.credit]] + payment_method_type = "DinersClub" +[[checkout.credit]] + payment_method_type = "Discover" +[[checkout.credit]] + payment_method_type = "CartesBancaires" +[[checkout.credit]] + payment_method_type = "UnionPay" +[[checkout.debit]] + payment_method_type = "Mastercard" +[[checkout.debit]] + payment_method_type = "Visa" +[[checkout.debit]] + payment_method_type = "Interac" +[[checkout.debit]] + payment_method_type = "AmericanExpress" +[[checkout.debit]] + payment_method_type = "JCB" +[[checkout.debit]] + payment_method_type = "DinersClub" +[[checkout.debit]] + payment_method_type = "Discover" +[[checkout.debit]] + payment_method_type = "CartesBancaires" +[[checkout.debit]] + payment_method_type = "UnionPay" +[[checkout.wallet]] + payment_method_type = "apple_pay" +[[checkout.wallet]] + payment_method_type = "google_pay" +[[checkout.wallet]] + payment_method_type = "paypal" +[checkout.connector_auth.SignatureKey] +api_key="Checkout API Public Key" +key1="Processing Channel ID" +api_secret="Checkout API Secret Key" +[checkout.connector_webhook_details] +merchant_secret="Source verification key" + +[checkout.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[checkout.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[checkout.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[coinbase] +[[coinbase.crypto]] + payment_method_type = "crypto_currency" +[coinbase.connector_auth.HeaderKey] +api_key="API Key" +[coinbase.connector_webhook_details] +merchant_secret="Source verification key" + +[cryptopay] +[[cryptopay.crypto]] + payment_method_type = "crypto_currency" +[cryptopay.connector_auth.BodyKey] +api_key="API Key" +key1="Secret Key" +[cryptopay.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource] +[[cybersource.credit]] + payment_method_type = "Mastercard" +[[cybersource.credit]] + payment_method_type = "Visa" +[[cybersource.credit]] + payment_method_type = "Interac" +[[cybersource.credit]] + payment_method_type = "AmericanExpress" +[[cybersource.credit]] + payment_method_type = "JCB" +[[cybersource.credit]] + payment_method_type = "DinersClub" +[[cybersource.credit]] + payment_method_type = "Discover" +[[cybersource.credit]] + payment_method_type = "CartesBancaires" +[[cybersource.credit]] + payment_method_type = "UnionPay" +[[cybersource.debit]] + payment_method_type = "Mastercard" +[[cybersource.debit]] + payment_method_type = "Visa" +[[cybersource.debit]] + payment_method_type = "Interac" +[[cybersource.debit]] + payment_method_type = "AmericanExpress" +[[cybersource.debit]] + payment_method_type = "JCB" +[[cybersource.debit]] + payment_method_type = "DinersClub" +[[cybersource.debit]] + payment_method_type = "Discover" +[[cybersource.debit]] + payment_method_type = "CartesBancaires" +[[cybersource.debit]] + payment_method_type = "UnionPay" +[[cybersource.wallet]] + payment_method_type = "apple_pay" +[[cybersource.wallet]] + payment_method_type = "google_pay" +[cybersource.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[cybersource.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[cybersource.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[cybersource.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[dlocal] +[[dlocal.credit]] + payment_method_type = "Mastercard" +[[dlocal.credit]] + payment_method_type = "Visa" +[[dlocal.credit]] + payment_method_type = "Interac" +[[dlocal.credit]] + payment_method_type = "AmericanExpress" +[[dlocal.credit]] + payment_method_type = "JCB" +[[dlocal.credit]] + payment_method_type = "DinersClub" +[[dlocal.credit]] + payment_method_type = "Discover" +[[dlocal.credit]] + payment_method_type = "CartesBancaires" +[[dlocal.credit]] + payment_method_type = "UnionPay" +[[dlocal.debit]] + payment_method_type = "Mastercard" +[[dlocal.debit]] + payment_method_type = "Visa" +[[dlocal.debit]] + payment_method_type = "Interac" +[[dlocal.debit]] + payment_method_type = "AmericanExpress" +[[dlocal.debit]] + payment_method_type = "JCB" +[[dlocal.debit]] + payment_method_type = "DinersClub" +[[dlocal.debit]] + payment_method_type = "Discover" +[[dlocal.debit]] + payment_method_type = "CartesBancaires" +[[dlocal.debit]] + payment_method_type = "UnionPay" +[dlocal.connector_auth.SignatureKey] +api_key="X Login" +key1="X Trans Key" +api_secret="Secret Key" +[dlocal.connector_webhook_details] +merchant_secret="Source verification key" + +[fiserv] +[[fiserv.credit]] + payment_method_type = "Mastercard" +[[fiserv.credit]] + payment_method_type = "Visa" +[[fiserv.credit]] + payment_method_type = "Interac" +[[fiserv.credit]] + payment_method_type = "AmericanExpress" +[[fiserv.credit]] + payment_method_type = "JCB" +[[fiserv.credit]] + payment_method_type = "DinersClub" +[[fiserv.credit]] + payment_method_type = "Discover" +[[fiserv.credit]] + payment_method_type = "CartesBancaires" +[[fiserv.credit]] + payment_method_type = "UnionPay" +[[fiserv.debit]] + payment_method_type = "Mastercard" +[[fiserv.debit]] + payment_method_type = "Visa" +[[fiserv.debit]] + payment_method_type = "Interac" +[[fiserv.debit]] + payment_method_type = "AmericanExpress" +[[fiserv.debit]] + payment_method_type = "JCB" +[[fiserv.debit]] + payment_method_type = "DinersClub" +[[fiserv.debit]] + payment_method_type = "Discover" +[[fiserv.debit]] + payment_method_type = "CartesBancaires" +[[fiserv.debit]] + payment_method_type = "UnionPay" +[fiserv.connector_auth.SignatureKey] +api_key="API Key" +key1="Merchant ID" +api_secret="API Secret" +[fiserv.metadata] +terminal_id="Terminal ID" +[fiserv.connector_webhook_details] +merchant_secret="Source verification key" + +[forte] +[[forte.credit]] + payment_method_type = "Mastercard" +[[forte.credit]] + payment_method_type = "Visa" +[[forte.credit]] + payment_method_type = "Interac" +[[forte.credit]] + payment_method_type = "AmericanExpress" +[[forte.credit]] + payment_method_type = "JCB" +[[forte.credit]] + payment_method_type = "DinersClub" +[[forte.credit]] + payment_method_type = "Discover" +[[forte.credit]] + payment_method_type = "CartesBancaires" +[[forte.credit]] + payment_method_type = "UnionPay" +[[forte.debit]] + payment_method_type = "Mastercard" +[[forte.debit]] + payment_method_type = "Visa" +[[forte.debit]] + payment_method_type = "Interac" +[[forte.debit]] + payment_method_type = "AmericanExpress" +[[forte.debit]] + payment_method_type = "JCB" +[[forte.debit]] + payment_method_type = "DinersClub" +[[forte.debit]] + payment_method_type = "Discover" +[[forte.debit]] + payment_method_type = "CartesBancaires" +[[forte.debit]] + payment_method_type = "UnionPay" +[forte.connector_auth.MultiAuthKey] +api_key="API Access ID" +key1="Organization ID" +api_secret="API Secure Key" +key2="Location ID" +[forte.connector_webhook_details] +merchant_secret="Source verification key" + +[globalpay] +[[globalpay.credit]] + payment_method_type = "Mastercard" +[[globalpay.credit]] + payment_method_type = "Visa" +[[globalpay.credit]] + payment_method_type = "Interac" +[[globalpay.credit]] + payment_method_type = "AmericanExpress" +[[globalpay.credit]] + payment_method_type = "JCB" +[[globalpay.credit]] + payment_method_type = "DinersClub" +[[globalpay.credit]] + payment_method_type = "Discover" +[[globalpay.credit]] + payment_method_type = "CartesBancaires" +[[globalpay.credit]] + payment_method_type = "UnionPay" +[[globalpay.debit]] + payment_method_type = "Mastercard" +[[globalpay.debit]] + payment_method_type = "Visa" +[[globalpay.debit]] + payment_method_type = "Interac" +[[globalpay.debit]] + payment_method_type = "AmericanExpress" +[[globalpay.debit]] + payment_method_type = "JCB" +[[globalpay.debit]] + payment_method_type = "DinersClub" +[[globalpay.debit]] + payment_method_type = "Discover" +[[globalpay.debit]] + payment_method_type = "CartesBancaires" +[[globalpay.debit]] + payment_method_type = "UnionPay" +[[globalpay.bank_redirect]] + payment_method_type = "ideal" +[[globalpay.bank_redirect]] + payment_method_type = "giropay" +[[globalpay.bank_redirect]] + payment_method_type = "sofort" +[[globalpay.bank_redirect]] + payment_method_type = "eps" +[[globalpay.wallet]] + payment_method_type = "google_pay" +[[globalpay.wallet]] + payment_method_type = "paypal" +[globalpay.connector_auth.BodyKey] +api_key="Global App Key" +key1="Global App ID" +[globalpay.metadata] +account_name="Account Name" +[globalpay.connector_webhook_details] +merchant_secret="Source verification key" +[globalpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[globepay] +[[globepay.wallet]] + payment_method_type = "we_chat_pay" +[[globepay.wallet]] + payment_method_type = "ali_pay" +[globepay.connector_auth.BodyKey] +api_key="Partner Code" +key1="Credential Code" +[globepay.connector_webhook_details] +merchant_secret="Source verification key" + +[gocardless] +[[gocardless.bank_debit]] + payment_method_type = "ach" +[[gocardless.bank_debit]] + payment_method_type = "becs" +[[gocardless.bank_debit]] + payment_method_type = "sepa" +[gocardless.connector_auth.HeaderKey] +api_key="Access Token" +[gocardless.connector_webhook_details] +merchant_secret="Source verification key" + +[iatapay] +[[iatapay.upi]] + payment_method_type = "upi_collect" +[iatapay.connector_auth.SignatureKey] +api_key="Client ID" +key1="Airline ID" +api_secret="Client Secret" +[iatapay.connector_webhook_details] +merchant_secret="Source verification key" + +[klarna] +[[klarna.pay_later]] + payment_method_type = "klarna" +[klarna.connector_auth.HeaderKey] +api_key="Klarna API Key" +[klarna.connector_webhook_details] +merchant_secret="Source verification key" + +[mollie] +[[mollie.credit]] + payment_method_type = "Mastercard" +[[mollie.credit]] + payment_method_type = "Visa" +[[mollie.credit]] + payment_method_type = "Interac" +[[mollie.credit]] + payment_method_type = "AmericanExpress" +[[mollie.credit]] + payment_method_type = "JCB" +[[mollie.credit]] + payment_method_type = "DinersClub" +[[mollie.credit]] + payment_method_type = "Discover" +[[mollie.credit]] + payment_method_type = "CartesBancaires" +[[mollie.credit]] + payment_method_type = "UnionPay" +[[mollie.debit]] + payment_method_type = "Mastercard" +[[mollie.debit]] + payment_method_type = "Visa" +[[mollie.debit]] + payment_method_type = "Interac" +[[mollie.debit]] + payment_method_type = "AmericanExpress" +[[mollie.debit]] + payment_method_type = "JCB" +[[mollie.debit]] + payment_method_type = "DinersClub" +[[mollie.debit]] + payment_method_type = "Discover" +[[mollie.debit]] + payment_method_type = "CartesBancaires" +[[mollie.debit]] + payment_method_type = "UnionPay" +[[mollie.bank_redirect]] + payment_method_type = "ideal" +[[mollie.bank_redirect]] + payment_method_type = "giropay" +[[mollie.bank_redirect]] + payment_method_type = "sofort" +[[mollie.bank_redirect]] + payment_method_type = "eps" +[[mollie.bank_redirect]] + payment_method_type = "przelewy24" +[[mollie.bank_redirect]] + payment_method_type = "bancontact_card" +[[mollie.wallet]] + payment_method_type = "paypal" +[mollie.connector_auth.BodyKey] +api_key="API Key" +key1="Profile Token" +[mollie.connector_webhook_details] +merchant_secret="Source verification key" + +[multisafepay] +[[multisafepay.credit]] + payment_method_type = "Mastercard" +[[multisafepay.credit]] + payment_method_type = "Visa" +[[multisafepay.credit]] + payment_method_type = "Interac" +[[multisafepay.credit]] + payment_method_type = "AmericanExpress" +[[multisafepay.credit]] + payment_method_type = "JCB" +[[multisafepay.credit]] + payment_method_type = "DinersClub" +[[multisafepay.credit]] + payment_method_type = "Discover" +[[multisafepay.credit]] + payment_method_type = "CartesBancaires" +[[multisafepay.credit]] + payment_method_type = "UnionPay" +[[multisafepay.debit]] + payment_method_type = "Mastercard" +[[multisafepay.debit]] + payment_method_type = "Visa" +[[multisafepay.debit]] + payment_method_type = "Interac" +[[multisafepay.debit]] + payment_method_type = "AmericanExpress" +[[multisafepay.debit]] + payment_method_type = "JCB" +[[multisafepay.debit]] + payment_method_type = "DinersClub" +[[multisafepay.debit]] + payment_method_type = "Discover" +[[multisafepay.debit]] + payment_method_type = "CartesBancaires" +[[multisafepay.debit]] + payment_method_type = "UnionPay" +[[multisafepay.wallet]] + payment_method_type = "google_pay" +[[multisafepay.wallet]] + payment_method_type = "paypal" +[multisafepay.connector_auth.HeaderKey] +api_key="Enter API Key" +[multisafepay.connector_webhook_details] +merchant_secret="Source verification key" +[multisafepay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nexinets] +[[nexinets.credit]] + payment_method_type = "Mastercard" +[[nexinets.credit]] + payment_method_type = "Visa" +[[nexinets.credit]] + payment_method_type = "Interac" +[[nexinets.credit]] + payment_method_type = "AmericanExpress" +[[nexinets.credit]] + payment_method_type = "JCB" +[[nexinets.credit]] + payment_method_type = "DinersClub" +[[nexinets.credit]] + payment_method_type = "Discover" +[[nexinets.credit]] + payment_method_type = "CartesBancaires" +[[nexinets.credit]] + payment_method_type = "UnionPay" +[[nexinets.debit]] + payment_method_type = "Mastercard" +[[nexinets.debit]] + payment_method_type = "Visa" +[[nexinets.debit]] + payment_method_type = "Interac" +[[nexinets.debit]] + payment_method_type = "AmericanExpress" +[[nexinets.debit]] + payment_method_type = "JCB" +[[nexinets.debit]] + payment_method_type = "DinersClub" +[[nexinets.debit]] + payment_method_type = "Discover" +[[nexinets.debit]] + payment_method_type = "CartesBancaires" +[[nexinets.debit]] + payment_method_type = "UnionPay" +[[nexinets.bank_redirect]] + payment_method_type = "ideal" +[[nexinets.bank_redirect]] + payment_method_type = "giropay" +[[nexinets.bank_redirect]] + payment_method_type = "sofort" +[[nexinets.bank_redirect]] + payment_method_type = "eps" +[[nexinets.wallet]] + payment_method_type = "apple_pay" +[[nexinets.wallet]] + payment_method_type = "paypal" +[nexinets.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant ID" +[nexinets.connector_webhook_details] +merchant_secret="Source verification key" +[nexinets.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nexinets.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[nmi] +[[nmi.credit]] + payment_method_type = "Mastercard" +[[nmi.credit]] + payment_method_type = "Visa" +[[nmi.credit]] + payment_method_type = "Interac" +[[nmi.credit]] + payment_method_type = "AmericanExpress" +[[nmi.credit]] + payment_method_type = "JCB" +[[nmi.credit]] + payment_method_type = "DinersClub" +[[nmi.credit]] + payment_method_type = "Discover" +[[nmi.credit]] + payment_method_type = "CartesBancaires" +[[nmi.credit]] + payment_method_type = "UnionPay" +[[nmi.debit]] + payment_method_type = "Mastercard" +[[nmi.debit]] + payment_method_type = "Visa" +[[nmi.debit]] + payment_method_type = "Interac" +[[nmi.debit]] + payment_method_type = "AmericanExpress" +[[nmi.debit]] + payment_method_type = "JCB" +[[nmi.debit]] + payment_method_type = "DinersClub" +[[nmi.debit]] + payment_method_type = "Discover" +[[nmi.debit]] + payment_method_type = "CartesBancaires" +[[nmi.debit]] + payment_method_type = "UnionPay" +[[nmi.bank_redirect]] + payment_method_type = "ideal" +[[nmi.wallet]] + payment_method_type = "apple_pay" +[[nmi.wallet]] + payment_method_type = "google_pay" +[nmi.connector_auth.BodyKey] +api_key="API Key" +key1="Public Key" +[nmi.connector_webhook_details] +merchant_secret="Source verification key" + +[nmi.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nmi.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nmi.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[noon] +[[noon.credit]] + payment_method_type = "Mastercard" +[[noon.credit]] + payment_method_type = "Visa" +[[noon.credit]] + payment_method_type = "Interac" +[[noon.credit]] + payment_method_type = "AmericanExpress" +[[noon.credit]] + payment_method_type = "JCB" +[[noon.credit]] + payment_method_type = "DinersClub" +[[noon.credit]] + payment_method_type = "Discover" +[[noon.credit]] + payment_method_type = "CartesBancaires" +[[noon.credit]] + payment_method_type = "UnionPay" +[[noon.debit]] + payment_method_type = "Mastercard" +[[noon.debit]] + payment_method_type = "Visa" +[[noon.debit]] + payment_method_type = "Interac" +[[noon.debit]] + payment_method_type = "AmericanExpress" +[[noon.debit]] + payment_method_type = "JCB" +[[noon.debit]] + payment_method_type = "DinersClub" +[[noon.debit]] + payment_method_type = "Discover" +[[noon.debit]] + payment_method_type = "CartesBancaires" +[[noon.debit]] + payment_method_type = "UnionPay" +[[noon.wallet]] + payment_method_type = "apple_pay" +[[noon.wallet]] + payment_method_type = "google_pay" +[[noon.wallet]] + payment_method_type = "paypal" +[noon.connector_auth.SignatureKey] +api_key="API Key" +key1="Business Identifier" +api_secret="Application Identifier" +[noon.connector_webhook_details] +merchant_secret="Source verification key" + +[noon.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[noon.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[noon.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[nuvei] +[[nuvei.credit]] + payment_method_type = "Mastercard" +[[nuvei.credit]] + payment_method_type = "Visa" +[[nuvei.credit]] + payment_method_type = "Interac" +[[nuvei.credit]] + payment_method_type = "AmericanExpress" +[[nuvei.credit]] + payment_method_type = "JCB" +[[nuvei.credit]] + payment_method_type = "DinersClub" +[[nuvei.credit]] + payment_method_type = "Discover" +[[nuvei.credit]] + payment_method_type = "CartesBancaires" +[[nuvei.credit]] + payment_method_type = "UnionPay" +[[nuvei.debit]] + payment_method_type = "Mastercard" +[[nuvei.debit]] + payment_method_type = "Visa" +[[nuvei.debit]] + payment_method_type = "Interac" +[[nuvei.debit]] + payment_method_type = "AmericanExpress" +[[nuvei.debit]] + payment_method_type = "JCB" +[[nuvei.debit]] + payment_method_type = "DinersClub" +[[nuvei.debit]] + payment_method_type = "Discover" +[[nuvei.debit]] + payment_method_type = "CartesBancaires" +[[nuvei.debit]] + payment_method_type = "UnionPay" +[[nuvei.pay_later]] + payment_method_type = "klarna" +[[nuvei.pay_later]] + payment_method_type = "afterpay_clearpay" +[[nuvei.bank_redirect]] + payment_method_type = "ideal" +[[nuvei.bank_redirect]] + payment_method_type = "giropay" +[[nuvei.bank_redirect]] + payment_method_type = "sofort" +[[nuvei.bank_redirect]] + payment_method_type = "eps" +[[nuvei.wallet]] + payment_method_type = "apple_pay" +[[nuvei.wallet]] + payment_method_type = "google_pay" +[[nuvei.wallet]] + payment_method_type = "paypal" +[nuvei.connector_auth.SignatureKey] +api_key="Merchant ID" +key1="Merchant Site ID" +api_secret="Merchant Secret" +[nuvei.connector_webhook_details] +merchant_secret="Source verification key" + +[nuvei.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nuvei.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nuvei.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + +[opennode] +[[opennode.crypto]] + payment_method_type = "crypto_currency" +[opennode.connector_auth.HeaderKey] +api_key="API Key" +[opennode.connector_webhook_details] +merchant_secret="Source verification key" + +[prophetpay] +[[prophetpay.card_redirect]] + payment_method_type = "card_redirect" +[prophetpay.connector_auth.SignatureKey] +api_key="Username" +key1="Token" +api_secret="Profile" + +[payme] +[[payme.credit]] + payment_method_type = "Mastercard" +[[payme.credit]] + payment_method_type = "Visa" +[[payme.credit]] + payment_method_type = "Interac" +[[payme.credit]] + payment_method_type = "AmericanExpress" +[[payme.credit]] + payment_method_type = "JCB" +[[payme.credit]] + payment_method_type = "DinersClub" +[[payme.credit]] + payment_method_type = "Discover" +[[payme.credit]] + payment_method_type = "CartesBancaires" +[[payme.credit]] + payment_method_type = "UnionPay" +[[payme.debit]] + payment_method_type = "Mastercard" +[[payme.debit]] + payment_method_type = "Visa" +[[payme.debit]] + payment_method_type = "Interac" +[[payme.debit]] + payment_method_type = "AmericanExpress" +[[payme.debit]] + payment_method_type = "JCB" +[[payme.debit]] + payment_method_type = "DinersClub" +[[payme.debit]] + payment_method_type = "Discover" +[[payme.debit]] + payment_method_type = "CartesBancaires" +[[payme.debit]] + payment_method_type = "UnionPay" +[payme.connector_auth.BodyKey] +api_key="Seller Payme Id" +key1="Payme Public Key" +[payme.connector_webhook_details] +merchant_secret="Payme Client Secret" +additional_secret="Payme Client Key" + +[paypal] +[[paypal.credit]] + payment_method_type = "Mastercard" +[[paypal.credit]] + payment_method_type = "Visa" +[[paypal.credit]] + payment_method_type = "Interac" +[[paypal.credit]] + payment_method_type = "AmericanExpress" +[[paypal.credit]] + payment_method_type = "JCB" +[[paypal.credit]] + payment_method_type = "DinersClub" +[[paypal.credit]] + payment_method_type = "Discover" +[[paypal.credit]] + payment_method_type = "CartesBancaires" +[[paypal.credit]] + payment_method_type = "UnionPay" +[[paypal.debit]] + payment_method_type = "Mastercard" +[[paypal.debit]] + payment_method_type = "Visa" +[[paypal.debit]] + payment_method_type = "Interac" +[[paypal.debit]] + payment_method_type = "AmericanExpress" +[[paypal.debit]] + payment_method_type = "JCB" +[[paypal.debit]] + payment_method_type = "DinersClub" +[[paypal.debit]] + payment_method_type = "Discover" +[[paypal.debit]] + payment_method_type = "CartesBancaires" +[[paypal.debit]] + payment_method_type = "UnionPay" +[[paypal.wallet]] + payment_method_type = "paypal" +[[paypal.bank_redirect]] + payment_method_type = "ideal" +[[paypal.bank_redirect]] + payment_method_type = "giropay" +[[paypal.bank_redirect]] + payment_method_type = "sofort" +[[paypal.bank_redirect]] + payment_method_type = "eps" +is_verifiable = true +[paypal.connector_auth.BodyKey] +api_key="Client Secret" +key1="Client ID" +[paypal.connector_webhook_details] +merchant_secret="Source verification key" + + +[payu] +[[payu.credit]] + payment_method_type = "Mastercard" +[[payu.credit]] + payment_method_type = "Visa" +[[payu.credit]] + payment_method_type = "Interac" +[[payu.credit]] + payment_method_type = "AmericanExpress" +[[payu.credit]] + payment_method_type = "JCB" +[[payu.credit]] + payment_method_type = "DinersClub" +[[payu.credit]] + payment_method_type = "Discover" +[[payu.credit]] + payment_method_type = "CartesBancaires" +[[payu.credit]] + payment_method_type = "UnionPay" +[[payu.debit]] + payment_method_type = "Mastercard" +[[payu.debit]] + payment_method_type = "Visa" +[[payu.debit]] + payment_method_type = "Interac" +[[payu.debit]] + payment_method_type = "AmericanExpress" +[[payu.debit]] + payment_method_type = "JCB" +[[payu.debit]] + payment_method_type = "DinersClub" +[[payu.debit]] + payment_method_type = "Discover" +[[payu.debit]] + payment_method_type = "CartesBancaires" +[[payu.debit]] + payment_method_type = "UnionPay" +[[payu.wallet]] + payment_method_type = "google_pay" +[payu.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant POS ID" +[payu.connector_webhook_details] +merchant_secret="Source verification key" + +[payu.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[placetopay] +[[placetopay.credit]] + payment_method_type = "Mastercard" +[[placetopay.credit]] + payment_method_type = "Visa" +[[placetopay.credit]] + payment_method_type = "Interac" +[[placetopay.credit]] + payment_method_type = "AmericanExpress" +[[placetopay.credit]] + payment_method_type = "JCB" +[[placetopay.credit]] + payment_method_type = "DinersClub" +[[placetopay.credit]] + payment_method_type = "Discover" +[[placetopay.credit]] + payment_method_type = "CartesBancaires" +[[placetopay.credit]] + payment_method_type = "UnionPay" +[[placetopay.debit]] + payment_method_type = "Mastercard" +[[placetopay.debit]] + payment_method_type = "Visa" +[[placetopay.debit]] + payment_method_type = "Interac" +[[placetopay.debit]] + payment_method_type = "AmericanExpress" +[[placetopay.debit]] + payment_method_type = "JCB" +[[placetopay.debit]] + payment_method_type = "DinersClub" +[[placetopay.debit]] + payment_method_type = "Discover" +[[placetopay.debit]] + payment_method_type = "CartesBancaires" +[[placetopay.debit]] + payment_method_type = "UnionPay" +[placetopay.connector_auth.BodyKey] +api_key="Login" +key1="Trankey" + +[powertranz] +[[powertranz.credit]] + payment_method_type = "Mastercard" +[[powertranz.credit]] + payment_method_type = "Visa" +[[powertranz.credit]] + payment_method_type = "Interac" +[[powertranz.credit]] + payment_method_type = "AmericanExpress" +[[powertranz.credit]] + payment_method_type = "JCB" +[[powertranz.credit]] + payment_method_type = "DinersClub" +[[powertranz.credit]] + payment_method_type = "Discover" +[[powertranz.credit]] + payment_method_type = "CartesBancaires" +[[powertranz.credit]] + payment_method_type = "UnionPay" +[[powertranz.debit]] + payment_method_type = "Mastercard" +[[powertranz.debit]] + payment_method_type = "Visa" +[[powertranz.debit]] + payment_method_type = "Interac" +[[powertranz.debit]] + payment_method_type = "AmericanExpress" +[[powertranz.debit]] + payment_method_type = "JCB" +[[powertranz.debit]] + payment_method_type = "DinersClub" +[[powertranz.debit]] + payment_method_type = "Discover" +[[powertranz.debit]] + payment_method_type = "CartesBancaires" +[[powertranz.debit]] + payment_method_type = "UnionPay" +[powertranz.connector_auth.BodyKey] +key1 = "PowerTranz Id" +api_key="PowerTranz Password" +[powertranz.connector_webhook_details] +merchant_secret="Source verification key" + +[rapyd] +[[rapyd.credit]] + payment_method_type = "Mastercard" +[[rapyd.credit]] + payment_method_type = "Visa" +[[rapyd.credit]] + payment_method_type = "Interac" +[[rapyd.credit]] + payment_method_type = "AmericanExpress" +[[rapyd.credit]] + payment_method_type = "JCB" +[[rapyd.credit]] + payment_method_type = "DinersClub" +[[rapyd.credit]] + payment_method_type = "Discover" +[[rapyd.credit]] + payment_method_type = "CartesBancaires" +[[rapyd.credit]] + payment_method_type = "UnionPay" +[[rapyd.debit]] + payment_method_type = "Mastercard" +[[rapyd.debit]] + payment_method_type = "Visa" +[[rapyd.debit]] + payment_method_type = "Interac" +[[rapyd.debit]] + payment_method_type = "AmericanExpress" +[[rapyd.debit]] + payment_method_type = "JCB" +[[rapyd.debit]] + payment_method_type = "DinersClub" +[[rapyd.debit]] + payment_method_type = "Discover" +[[rapyd.debit]] + payment_method_type = "CartesBancaires" +[[rapyd.debit]] + payment_method_type = "UnionPay" +[[rapyd.wallet]] + payment_method_type = "apple_pay" +[rapyd.connector_auth.BodyKey] +api_key="Access Key" +key1="API Secret" +[rapyd.connector_webhook_details] +merchant_secret="Source verification key" + +[rapyd.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[rapyd.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[shift4] +[[shift4.credit]] + payment_method_type = "Mastercard" +[[shift4.credit]] + payment_method_type = "Visa" +[[shift4.credit]] + payment_method_type = "Interac" +[[shift4.credit]] + payment_method_type = "AmericanExpress" +[[shift4.credit]] + payment_method_type = "JCB" +[[shift4.credit]] + payment_method_type = "DinersClub" +[[shift4.credit]] + payment_method_type = "Discover" +[[shift4.credit]] + payment_method_type = "CartesBancaires" +[[shift4.credit]] + payment_method_type = "UnionPay" +[[shift4.debit]] + payment_method_type = "Mastercard" +[[shift4.debit]] + payment_method_type = "Visa" +[[shift4.debit]] + payment_method_type = "Interac" +[[shift4.debit]] + payment_method_type = "AmericanExpress" +[[shift4.debit]] + payment_method_type = "JCB" +[[shift4.debit]] + payment_method_type = "DinersClub" +[[shift4.debit]] + payment_method_type = "Discover" +[[shift4.debit]] + payment_method_type = "CartesBancaires" +[[shift4.debit]] + payment_method_type = "UnionPay" +[[shift4.bank_redirect]] + payment_method_type = "ideal" +[[shift4.bank_redirect]] + payment_method_type = "giropay" +[[shift4.bank_redirect]] + payment_method_type = "sofort" +[[shift4.bank_redirect]] + payment_method_type = "eps" +[shift4.connector_auth.HeaderKey] +api_key="API Key" +[shift4.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe] +[[stripe.credit]] + payment_method_type = "Mastercard" +[[stripe.credit]] + payment_method_type = "Visa" +[[stripe.credit]] + payment_method_type = "Interac" +[[stripe.credit]] + payment_method_type = "AmericanExpress" +[[stripe.credit]] + payment_method_type = "JCB" +[[stripe.credit]] + payment_method_type = "DinersClub" +[[stripe.credit]] + payment_method_type = "Discover" +[[stripe.credit]] + payment_method_type = "CartesBancaires" +[[stripe.credit]] + payment_method_type = "UnionPay" +[[stripe.debit]] + payment_method_type = "Mastercard" +[[stripe.debit]] + payment_method_type = "Visa" +[[stripe.debit]] + payment_method_type = "Interac" +[[stripe.debit]] + payment_method_type = "AmericanExpress" +[[stripe.debit]] + payment_method_type = "JCB" +[[stripe.debit]] + payment_method_type = "DinersClub" +[[stripe.debit]] + payment_method_type = "Discover" +[[stripe.debit]] + payment_method_type = "CartesBancaires" +[[stripe.debit]] + payment_method_type = "UnionPay" +[[stripe.pay_later]] + payment_method_type = "klarna" +[[stripe.pay_later]] + payment_method_type = "affirm" +[[stripe.pay_later]] + payment_method_type = "afterpay_clearpay" +[[stripe.bank_redirect]] + payment_method_type = "ideal" +[[stripe.bank_redirect]] + payment_method_type = "giropay" +[[stripe.bank_redirect]] + payment_method_type = "sofort" +[[stripe.bank_redirect]] + payment_method_type = "eps" +[[stripe.bank_redirect]] + payment_method_type = "bancontact_card" +[[stripe.bank_redirect]] + payment_method_type = "przelewy24" +[[stripe.bank_debit]] + payment_method_type = "ach" +[[stripe.bank_debit]] + payment_method_type = "bacs" +[[stripe.bank_debit]] + payment_method_type = "becs" +[[stripe.bank_debit]] + payment_method_type = "sepa" +[[stripe.bank_transfer]] + payment_method_type = "ach" +[[stripe.bank_transfer]] + payment_method_type = "bacs" +[[stripe.bank_transfer]] + payment_method_type = "sepa" +[[stripe.bank_transfer]] + payment_method_type = "multibanco" +[[stripe.wallet]] + payment_method_type = "apple_pay" +[[stripe.wallet]] + payment_method_type = "google_pay" +[[stripe.wallet]] + payment_method_type = "we_chat_pay" +[[stripe.wallet]] + payment_method_type = "ali_pay" +[[stripe.wallet]] + payment_method_type = "cashapp" +is_verifiable = true +[stripe.connector_auth.HeaderKey] +api_key="Secret Key" +[stripe.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +stripe_publishable_key="Stripe Publishable Key" +merchant_id="Google Pay Merchant ID" + +[stripe.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[stripe.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[stax] +[[stax.credit]] + payment_method_type = "Mastercard" +[[stax.credit]] + payment_method_type = "Visa" +[[stax.credit]] + payment_method_type = "Interac" +[[stax.credit]] + payment_method_type = "AmericanExpress" +[[stax.credit]] + payment_method_type = "JCB" +[[stax.credit]] + payment_method_type = "DinersClub" +[[stax.credit]] + payment_method_type = "Discover" +[[stax.credit]] + payment_method_type = "CartesBancaires" +[[stax.credit]] + payment_method_type = "UnionPay" +[[stax.debit]] + payment_method_type = "Mastercard" +[[stax.debit]] + payment_method_type = "Visa" +[[stax.debit]] + payment_method_type = "Interac" +[[stax.debit]] + payment_method_type = "AmericanExpress" +[[stax.debit]] + payment_method_type = "JCB" +[[stax.debit]] + payment_method_type = "DinersClub" +[[stax.debit]] + payment_method_type = "Discover" +[[stax.debit]] + payment_method_type = "CartesBancaires" +[[stax.debit]] + payment_method_type = "UnionPay" +[[stax.bank_debit]] + payment_method_type = "ach" +[stax.connector_auth.HeaderKey] +api_key="Api Key" +[stax.connector_webhook_details] +merchant_secret="Source verification key" + +[square] +[[square.credit]] + payment_method_type = "Mastercard" +[[square.credit]] + payment_method_type = "Visa" +[[square.credit]] + payment_method_type = "Interac" +[[square.credit]] + payment_method_type = "AmericanExpress" +[[square.credit]] + payment_method_type = "JCB" +[[square.credit]] + payment_method_type = "DinersClub" +[[square.credit]] + payment_method_type = "Discover" +[[square.credit]] + payment_method_type = "CartesBancaires" +[[square.credit]] + payment_method_type = "UnionPay" +[[square.debit]] + payment_method_type = "Mastercard" +[[square.debit]] + payment_method_type = "Visa" +[[square.debit]] + payment_method_type = "Interac" +[[square.debit]] + payment_method_type = "AmericanExpress" +[[square.debit]] + payment_method_type = "JCB" +[[square.debit]] + payment_method_type = "DinersClub" +[[square.debit]] + payment_method_type = "Discover" +[[square.debit]] + payment_method_type = "CartesBancaires" +[[square.debit]] + payment_method_type = "UnionPay" +[square_payout.connector_auth.BodyKey] +api_key = "Square API Key" +key1 = "Square Client Id" +[square.connector_webhook_details] +merchant_secret="Source verification key" + +[trustpay] +[[trustpay.credit]] + payment_method_type = "Mastercard" +[[trustpay.credit]] + payment_method_type = "Visa" +[[trustpay.credit]] + payment_method_type = "Interac" +[[trustpay.credit]] + payment_method_type = "AmericanExpress" +[[trustpay.credit]] + payment_method_type = "JCB" +[[trustpay.credit]] + payment_method_type = "DinersClub" +[[trustpay.credit]] + payment_method_type = "Discover" +[[trustpay.credit]] + payment_method_type = "CartesBancaires" +[[trustpay.credit]] + payment_method_type = "UnionPay" +[[trustpay.debit]] + payment_method_type = "Mastercard" +[[trustpay.debit]] + payment_method_type = "Visa" +[[trustpay.debit]] + payment_method_type = "Interac" +[[trustpay.debit]] + payment_method_type = "AmericanExpress" +[[trustpay.debit]] + payment_method_type = "JCB" +[[trustpay.debit]] + payment_method_type = "DinersClub" +[[trustpay.debit]] + payment_method_type = "Discover" +[[trustpay.debit]] + payment_method_type = "CartesBancaires" +[[trustpay.debit]] + payment_method_type = "UnionPay" +[[trustpay.bank_redirect]] + payment_method_type = "ideal" +[[trustpay.bank_redirect]] + payment_method_type = "giropay" +[[trustpay.bank_redirect]] + payment_method_type = "sofort" +[[trustpay.bank_redirect]] + payment_method_type = "eps" +[[trustpay.bank_redirect]] + payment_method_type = "blik" +[[trustpay.wallet]] + payment_method_type = "apple_pay" +[[trustpay.wallet]] + payment_method_type = "google_pay" +[trustpay.connector_auth.SignatureKey] +api_key="API Key" +key1="Project ID" +api_secret="Secret Key" +[trustpay.connector_webhook_details] +merchant_secret="Source verification key" + +[trustpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[trustpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[tsys] +[[tsys.credit]] + payment_method_type = "Mastercard" +[[tsys.credit]] + payment_method_type = "Visa" +[[tsys.credit]] + payment_method_type = "Interac" +[[tsys.credit]] + payment_method_type = "AmericanExpress" +[[tsys.credit]] + payment_method_type = "JCB" +[[tsys.credit]] + payment_method_type = "DinersClub" +[[tsys.credit]] + payment_method_type = "Discover" +[[tsys.credit]] + payment_method_type = "CartesBancaires" +[[tsys.credit]] + payment_method_type = "UnionPay" +[[tsys.debit]] + payment_method_type = "Mastercard" +[[tsys.debit]] + payment_method_type = "Visa" +[[tsys.debit]] + payment_method_type = "Interac" +[[tsys.debit]] + payment_method_type = "AmericanExpress" +[[tsys.debit]] + payment_method_type = "JCB" +[[tsys.debit]] + payment_method_type = "DinersClub" +[[tsys.debit]] + payment_method_type = "Discover" +[[tsys.debit]] + payment_method_type = "CartesBancaires" +[[tsys.debit]] + payment_method_type = "UnionPay" +[tsys.connector_auth.SignatureKey] +api_key="Device Id" +key1="Transaction Key" +api_secret="Developer Id" +[tsys.connector_webhook_details] +merchant_secret="Source verification key" + +[volt] +[[volt.bank_redirect]] + payment_method_type = "open_banking_uk" +[volt.connector_auth.MultiAuthKey] +api_key = "Username" +api_secret = "Password" +key1 = "Client ID" +key2 = "Client Secret" +[volt.connector_webhook_details] +merchant_secret="Source verification key" + +[worldline] +[[worldline.credit]] + payment_method_type = "Mastercard" +[[worldline.credit]] + payment_method_type = "Visa" +[[worldline.credit]] + payment_method_type = "Interac" +[[worldline.credit]] + payment_method_type = "AmericanExpress" +[[worldline.credit]] + payment_method_type = "JCB" +[[worldline.credit]] + payment_method_type = "DinersClub" +[[worldline.credit]] + payment_method_type = "Discover" +[[worldline.credit]] + payment_method_type = "CartesBancaires" +[[worldline.credit]] + payment_method_type = "UnionPay" +[[worldline.debit]] + payment_method_type = "Mastercard" +[[worldline.debit]] + payment_method_type = "Visa" +[[worldline.debit]] + payment_method_type = "Interac" +[[worldline.debit]] + payment_method_type = "AmericanExpress" +[[worldline.debit]] + payment_method_type = "JCB" +[[worldline.debit]] + payment_method_type = "DinersClub" +[[worldline.debit]] + payment_method_type = "Discover" +[[worldline.debit]] + payment_method_type = "CartesBancaires" +[[worldline.debit]] + payment_method_type = "UnionPay" +[[worldline.bank_redirect]] + payment_method_type = "ideal" +[[worldline.bank_redirect]] + payment_method_type = "giropay" +[worldline.connector_auth.SignatureKey] +api_key="API Key ID" +key1="Merchant ID" +api_secret="Secret API Key" +[worldline.connector_webhook_details] +merchant_secret="Source verification key" + +[worldpay] +[[worldpay.credit]] + payment_method_type = "Mastercard" +[[worldpay.credit]] + payment_method_type = "Visa" +[[worldpay.credit]] + payment_method_type = "Interac" +[[worldpay.credit]] + payment_method_type = "AmericanExpress" +[[worldpay.credit]] + payment_method_type = "JCB" +[[worldpay.credit]] + payment_method_type = "DinersClub" +[[worldpay.credit]] + payment_method_type = "Discover" +[[worldpay.credit]] + payment_method_type = "CartesBancaires" +[[worldpay.credit]] + payment_method_type = "UnionPay" +[[worldpay.debit]] + payment_method_type = "Mastercard" +[[worldpay.debit]] + payment_method_type = "Visa" +[[worldpay.debit]] + payment_method_type = "Interac" +[[worldpay.debit]] + payment_method_type = "AmericanExpress" +[[worldpay.debit]] + payment_method_type = "JCB" +[[worldpay.debit]] + payment_method_type = "DinersClub" +[[worldpay.debit]] + payment_method_type = "Discover" +[[worldpay.debit]] + payment_method_type = "CartesBancaires" +[[worldpay.debit]] + payment_method_type = "UnionPay" +[[worldpay.wallet]] + payment_method_type = "google_pay" +[[worldpay.wallet]] + payment_method_type = "apple_pay" +[worldpay.connector_auth.BodyKey] +api_key="Username" +key1="Password" +[worldpay.connector_webhook_details] +merchant_secret="Source verification key" + +[worldpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[worldpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[worldpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[zen] +[[zen.credit]] + payment_method_type = "Mastercard" +[[zen.credit]] + payment_method_type = "Visa" +[[zen.credit]] + payment_method_type = "Interac" +[[zen.credit]] + payment_method_type = "AmericanExpress" +[[zen.credit]] + payment_method_type = "JCB" +[[zen.credit]] + payment_method_type = "DinersClub" +[[zen.credit]] + payment_method_type = "Discover" +[[zen.credit]] + payment_method_type = "CartesBancaires" +[[zen.credit]] + payment_method_type = "UnionPay" +[[zen.debit]] + payment_method_type = "Mastercard" +[[zen.debit]] + payment_method_type = "Visa" +[[zen.debit]] + payment_method_type = "Interac" +[[zen.debit]] + payment_method_type = "AmericanExpress" +[[zen.debit]] + payment_method_type = "JCB" +[[zen.debit]] + payment_method_type = "DinersClub" +[[zen.debit]] + payment_method_type = "Discover" +[[zen.debit]] + payment_method_type = "CartesBancaires" +[[zen.debit]] + payment_method_type = "UnionPay" +[[zen.voucher]] + payment_method_type = "boleto" +[[zen.voucher]] + payment_method_type = "efecty" +[[zen.voucher]] + payment_method_type = "pago_efectivo" +[[zen.voucher]] + payment_method_type = "red_compra" +[[zen.voucher]] + payment_method_type = "red_pagos" +[[zen.bank_transfer]] + payment_method_type = "pix" +[[zen.bank_transfer]] + payment_method_type = "pse" +[[zen.wallet]] + payment_method_type = "apple_pay" +[[zen.wallet]] + payment_method_type = "google_pay" +[zen.connector_auth.HeaderKey] +api_key="API Key" +[zen.connector_webhook_details] +merchant_secret="Source verification key" + +[zen.metadata.apple_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" +[zen.metadata.google_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" + + +[dummy_connector] +[[dummy_connector.credit]] + payment_method_type = "Mastercard" +[[dummy_connector.credit]] + payment_method_type = "Visa" +[[dummy_connector.credit]] + payment_method_type = "Interac" +[[dummy_connector.credit]] + payment_method_type = "AmericanExpress" +[[dummy_connector.credit]] + payment_method_type = "JCB" +[[dummy_connector.credit]] + payment_method_type = "DinersClub" +[[dummy_connector.credit]] + payment_method_type = "Discover" +[[dummy_connector.credit]] + payment_method_type = "CartesBancaires" +[[dummy_connector.credit]] + payment_method_type = "UnionPay" +[[dummy_connector.debit]] + payment_method_type = "Mastercard" +[[dummy_connector.debit]] + payment_method_type = "Visa" +[[dummy_connector.debit]] + payment_method_type = "Interac" +[[dummy_connector.debit]] + payment_method_type = "AmericanExpress" +[[dummy_connector.debit]] + payment_method_type = "JCB" +[[dummy_connector.debit]] + payment_method_type = "DinersClub" +[[dummy_connector.debit]] + payment_method_type = "Discover" +[[dummy_connector.debit]] + payment_method_type = "CartesBancaires" +[[dummy_connector.debit]] + payment_method_type = "UnionPay" +[dummy_connector.connector_auth.HeaderKey] +api_key="Api Key" + +[paypal_test] +[[paypal_test.credit]] + payment_method_type = "Mastercard" +[[paypal_test.credit]] + payment_method_type = "Visa" +[[paypal_test.credit]] + payment_method_type = "Interac" +[[paypal_test.credit]] + payment_method_type = "AmericanExpress" +[[paypal_test.credit]] + payment_method_type = "JCB" +[[paypal_test.credit]] + payment_method_type = "DinersClub" +[[paypal_test.credit]] + payment_method_type = "Discover" +[[paypal_test.credit]] + payment_method_type = "CartesBancaires" +[[paypal_test.credit]] + payment_method_type = "UnionPay" +[[paypal_test.debit]] + payment_method_type = "Mastercard" +[[paypal_test.debit]] + payment_method_type = "Visa" +[[paypal_test.debit]] + payment_method_type = "Interac" +[[paypal_test.debit]] + payment_method_type = "AmericanExpress" +[[paypal_test.debit]] + payment_method_type = "JCB" +[[paypal_test.debit]] + payment_method_type = "DinersClub" +[[paypal_test.debit]] + payment_method_type = "Discover" +[[paypal_test.debit]] + payment_method_type = "CartesBancaires" +[[paypal_test.debit]] + payment_method_type = "UnionPay" +[[paypal_test.wallet]] + payment_method_type = "paypal" +[paypal_test.connector_auth.HeaderKey] +api_key="Api Key" + +[stripe_test] +[[stripe_test.credit]] + payment_method_type = "Mastercard" +[[stripe_test.credit]] + payment_method_type = "Visa" +[[stripe_test.credit]] + payment_method_type = "Interac" +[[stripe_test.credit]] + payment_method_type = "AmericanExpress" +[[stripe_test.credit]] + payment_method_type = "JCB" +[[stripe_test.credit]] + payment_method_type = "DinersClub" +[[stripe_test.credit]] + payment_method_type = "Discover" +[[stripe_test.credit]] + payment_method_type = "CartesBancaires" +[[stripe_test.credit]] + payment_method_type = "UnionPay" +[[stripe_test.debit]] + payment_method_type = "Mastercard" +[[stripe_test.debit]] + payment_method_type = "Visa" +[[stripe_test.debit]] + payment_method_type = "Interac" +[[stripe_test.debit]] + payment_method_type = "AmericanExpress" +[[stripe_test.debit]] + payment_method_type = "JCB" +[[stripe_test.debit]] + payment_method_type = "DinersClub" +[[stripe_test.debit]] + payment_method_type = "Discover" +[[stripe_test.debit]] + payment_method_type = "CartesBancaires" +[[stripe_test.debit]] + payment_method_type = "UnionPay" +[[stripe_test.wallet]] + payment_method_type = "google_pay" +[[stripe_test.wallet]] + payment_method_type = "ali_pay" +[[stripe_test.wallet]] + payment_method_type = "we_chat_pay" +[[stripe_test.pay_later]] + payment_method_type = "klarna" +[[stripe_test.pay_later]] + payment_method_type = "affirm" +[[stripe_test.pay_later]] + payment_method_type = "afterpay_clearpay" +[[paypal_test.wallet]] + payment_method_type = "paypal" +[stripe_test.connector_auth.HeaderKey] +api_key="Api Key" + +[helcim] +[[helcim.credit]] + payment_method_type = "Mastercard" +[[helcim.credit]] + payment_method_type = "Visa" +[[helcim.credit]] + payment_method_type = "Interac" +[[helcim.credit]] + payment_method_type = "AmericanExpress" +[[helcim.credit]] + payment_method_type = "JCB" +[[helcim.credit]] + payment_method_type = "DinersClub" +[[helcim.credit]] + payment_method_type = "Discover" +[[helcim.credit]] + payment_method_type = "CartesBancaires" +[[helcim.credit]] + payment_method_type = "UnionPay" +[[helcim.debit]] + payment_method_type = "Mastercard" +[[helcim.debit]] + payment_method_type = "Visa" +[[helcim.debit]] + payment_method_type = "Interac" +[[helcim.debit]] + payment_method_type = "AmericanExpress" +[[helcim.debit]] + payment_method_type = "JCB" +[[helcim.debit]] + payment_method_type = "DinersClub" +[[helcim.debit]] + payment_method_type = "Discover" +[[helcim.debit]] + payment_method_type = "CartesBancaires" +[[helcim.debit]] + payment_method_type = "UnionPay" +[helcim.connector_auth.HeaderKey] +api_key="Api Key" + + + + +[adyen_payout] +[[adyen_payout.credit]] + payment_method_type = "Mastercard" +[[adyen_payout.credit]] + payment_method_type = "Visa" +[[adyen_payout.credit]] + payment_method_type = "Interac" +[[adyen_payout.credit]] + payment_method_type = "AmericanExpress" +[[adyen_payout.credit]] + payment_method_type = "JCB" +[[adyen_payout.credit]] + payment_method_type = "DinersClub" +[[adyen_payout.credit]] + payment_method_type = "Discover" +[[adyen_payout.credit]] + payment_method_type = "CartesBancaires" +[[adyen_payout.credit]] + payment_method_type = "UnionPay" +[[adyen_payout.debit]] + payment_method_type = "Mastercard" +[[adyen_payout.debit]] + payment_method_type = "Visa" +[[adyen_payout.debit]] + payment_method_type = "Interac" +[[adyen_payout.debit]] + payment_method_type = "AmericanExpress" +[[adyen_payout.debit]] + payment_method_type = "JCB" +[[adyen_payout.debit]] + payment_method_type = "DinersClub" +[[adyen_payout.debit]] + payment_method_type = "Discover" +[[adyen_payout.debit]] + payment_method_type = "CartesBancaires" +[[adyen_payout.debit]] + payment_method_type = "UnionPay" +[[adyen_payout.bank_transfer]] + payment_method_type = "ach" +[[adyen_payout.bank_transfer]] + payment_method_type = "bacs" +[[adyen_payout.bank_transfer]] + payment_method_type = "sepa" +[adyen_payout.connector_auth.SignatureKey] +api_key = "Adyen API Key (Payout creation)" +api_secret = "Adyen Key (Payout submission)" +key1 = "Adyen Account Id" + +[wise_payout] +[[wise_payout.bank_transfer]] + payment_method_type = "ach" +[[wise_payout.bank_transfer]] + payment_method_type = "bacs" +[[wise_payout.bank_transfer]] + payment_method_type = "sepa" +[wise_payout.connector_auth.BodyKey] +api_key = "Wise API Key" +key1 = "Wise Account Id" \ No newline at end of file diff --git a/crates/currency_conversion/Cargo.toml b/crates/currency_conversion/Cargo.toml new file mode 100644 index 000000000000..d2c99080bf64 --- /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 = { git = "https://github.com/varunsrin/rusty_money", rev = "bbc0150742a0fff905225ff11ee09388e9babdcc", features = ["iso", "crypto"] } +serde = { version = "1.0.193", 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..a84520dca0ad --- /dev/null +++ b/crates/currency_conversion/src/types.rs @@ -0,0 +1,226 @@ +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::AOA => iso::AOA, + Currency::ARS => iso::ARS, + Currency::AUD => iso::AUD, + Currency::AWG => iso::AWG, + Currency::AZN => iso::AZN, + Currency::BAM => iso::BAM, + Currency::BBD => iso::BBD, + Currency::BDT => iso::BDT, + Currency::BGN => iso::BGN, + 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::BYN => iso::BYN, + 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::CVE => iso::CVE, + 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::FKP => iso::FKP, + Currency::GBP => iso::GBP, + Currency::GEL => iso::GEL, + 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::IQD => iso::IQD, + 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::LYD => iso::LYD, + 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::MRU => iso::MRU, + Currency::MUR => iso::MUR, + Currency::MVR => iso::MVR, + Currency::MWK => iso::MWK, + Currency::MXN => iso::MXN, + Currency::MYR => iso::MYR, + Currency::MZN => iso::MZN, + 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::PAB => iso::PAB, + 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::RSD => iso::RSD, + Currency::RUB => iso::RUB, + Currency::RWF => iso::RWF, + Currency::SAR => iso::SAR, + Currency::SBD => iso::SBD, + Currency::SCR => iso::SCR, + Currency::SEK => iso::SEK, + Currency::SGD => iso::SGD, + Currency::SHP => iso::SHP, + Currency::SLE => iso::SLE, + Currency::SLL => iso::SLL, + Currency::SOS => iso::SOS, + Currency::SRD => iso::SRD, + Currency::SSP => iso::SSP, + Currency::STN => iso::STN, + Currency::SVC => iso::SVC, + Currency::SZL => iso::SZL, + Currency::THB => iso::THB, + Currency::TND => iso::TND, + Currency::TOP => iso::TOP, + Currency::TTD => iso::TTD, + Currency::TRY => iso::TRY, + Currency::TWD => iso::TWD, + Currency::TZS => iso::TZS, + Currency::UAH => iso::UAH, + Currency::UGX => iso::UGX, + Currency::USD => iso::USD, + Currency::UYU => iso::UYU, + Currency::UZS => iso::UZS, + Currency::VES => iso::VES, + Currency::VND => iso::VND, + Currency::VUV => iso::VUV, + Currency::WST => iso::WST, + Currency::XAF => iso::XAF, + Currency::XCD => iso::XCD, + Currency::XOF => iso::XOF, + Currency::XPF => iso::XPF, + Currency::YER => iso::YER, + Currency::ZAR => iso::ZAR, + Currency::ZMW => iso::ZMW, + } +} diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 57ae1ec1ec87..39bcc71341bc 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -17,12 +17,12 @@ api_models = { version = "0.1.0", path = "../api_models" } 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" } - +diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } # Third party deps async-trait = "0.1.68" error-stack = "0.3.1" -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } diff --git a/crates/data_models/src/errors.rs b/crates/data_models/src/errors.rs index 4f8229ea0c9b..bed1ab9ccbf5 100644 --- a/crates/data_models/src/errors.rs +++ b/crates/data_models/src/errors.rs @@ -1,3 +1,5 @@ +use diesel_models::errors::DatabaseError; + pub type StorageResult = error_stack::Result; #[derive(Debug, thiserror::Error)] @@ -6,7 +8,7 @@ pub enum StorageError { InitializationError, // TODO: deprecate this error type to use a domain error instead #[error("DatabaseError: {0:?}")] - DatabaseError(String), + DatabaseError(error_stack::Report), #[error("ValueNotFound: {0}")] ValueNotFound(String), #[error("DuplicateValue: {entity} already exists {key:?}")] @@ -22,6 +24,8 @@ pub enum StorageError { SerializationFailed, #[error("MockDb error")] MockDbError, + #[error("Kafka error")] + KafkaError, #[error("Customer with this id is Redacted")] CustomerRedacted, #[error("Deserialization failure")] diff --git a/crates/data_models/src/mandates.rs b/crates/data_models/src/mandates.rs index afdcda3a40e7..319a78cf661f 100644 --- a/crates/data_models/src/mandates.rs +++ b/crates/data_models/src/mandates.rs @@ -9,6 +9,13 @@ use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; use time::PrimitiveDateTime; +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub struct MandateDetails { + pub update_mandate_id: Option, + pub mandate_type: Option, +} + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum MandateDataType { @@ -16,6 +23,13 @@ pub enum MandateDataType { MultiUse(Option), } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum MandateTypeDetails { + MandateType(MandateDataType), + MandateDetails(MandateDetails), +} #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct MandateAmountData { pub amount: i64, @@ -29,6 +43,8 @@ pub struct MandateAmountData { // information about creating mandates #[derive(Default, Eq, PartialEq, Debug, Clone)] pub struct MandateData { + /// A way to update the mandate's payment method details + pub update_mandate_id: Option, /// A concent from the customer to store the payment method pub customer_acceptance: Option, /// A way to select the type of mandate used @@ -90,6 +106,7 @@ impl From for MandateData { Self { customer_acceptance: value.customer_acceptance.map(|d| d.into()), mandate_type: value.mandate_type.map(|d| d.into()), + update_mandate_id: value.update_mandate_id, } } } diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index 4e7a0923f6a9..713003d666b2 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -50,4 +50,9 @@ pub struct PaymentIntent { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: Option, + pub incremental_authorization_allowed: Option, + pub authorization_count: Option, + pub fingerprint_id: Option, + pub session_expiry: Option, } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 88fc7b3b524a..e6b9950b4595 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use super::PaymentIntent; -use crate::{errors, mandates::MandateDataType, ForeignIDRef}; +use crate::{errors, mandates::MandateTypeDetails, ForeignIDRef}; #[async_trait::async_trait] pub trait PaymentAttemptInterface { @@ -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, @@ -100,6 +107,7 @@ pub struct PaymentAttempt { pub attempt_id: String, pub status: storage_enums::AttemptStatus, pub amount: i64, + pub net_amount: i64, pub currency: Option, pub save_to_locker: Option, pub connector: Option, @@ -135,7 +143,7 @@ pub struct PaymentAttempt { pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, // providing a location to store mandate details intermediately for transaction - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub multiple_capture_count: Option, // reference to the payment at connector side @@ -145,6 +153,18 @@ pub struct PaymentAttempt { pub authentication_data: Option, pub encoded_data: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, +} + +impl PaymentAttempt { + pub fn get_total_amount(&self) -> i64 { + self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0) + } + pub fn get_total_surcharge_amount(&self) -> Option { + self.surcharge_amount + .map(|surcharge_amount| surcharge_amount + self.tax_amount.unwrap_or(0)) + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -164,6 +184,9 @@ pub struct PaymentAttemptNew { pub attempt_id: String, pub status: storage_enums::AttemptStatus, pub amount: i64, + /// amount + surcharge_amount + tax_amount + /// This field will always be derived before updating in the Database + pub net_amount: i64, pub currency: Option, // pub auto_capture: Option, pub save_to_locker: Option, @@ -198,7 +221,7 @@ pub struct PaymentAttemptNew { pub business_sub_label: Option, pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub connector_response_reference_id: Option, pub multiple_capture_count: Option, @@ -207,6 +230,21 @@ pub struct PaymentAttemptNew { pub authentication_data: Option, pub encoded_data: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, +} + +impl PaymentAttemptNew { + /// returns amount + surcharge_amount + tax_amount + pub fn calculate_net_amount(&self) -> i64 { + self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0) + } + + pub fn populate_derived_fields(self) -> Self { + let mut payment_attempt_new = self; + payment_attempt_new.net_amount = payment_attempt_new.calculate_net_amount(); + payment_attempt_new + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -260,6 +298,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 +327,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 +356,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 { @@ -342,6 +386,10 @@ pub enum PaymentAttemptUpdate { connector: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + amount_capturable: i64, + }, } impl ForeignIDRef for PaymentAttempt { diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 2c5914f5b37f..7470b5f85028 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -107,6 +107,11 @@ pub struct PaymentIntentNew { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: Option, + pub incremental_authorization_allowed: Option, + pub authorization_count: Option, + pub fingerprint_id: Option, + pub session_expiry: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -116,6 +121,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, + incremental_authorization_allowed: Option, updated_by: String, }, Update { @@ -157,6 +164,8 @@ pub enum PaymentIntentUpdate { metadata: Option, payment_confirm_source: Option, updated_by: String, + fingerprint_id: Option, + session_expiry: Option, }, PaymentAttemptAndAttemptCountUpdate { active_attempt_id: String, @@ -182,6 +191,12 @@ pub enum PaymentIntentUpdate { surcharge_applicable: bool, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + }, + AuthorizationCountUpdate { + authorization_count: i32, + }, } #[derive(Clone, Debug, Default)] @@ -213,6 +228,10 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, + pub incremental_authorization_allowed: Option, + pub authorization_count: Option, + pub fingerprint_id: Option, + pub session_expiry: Option, } impl From for PaymentIntentUpdateInternal { @@ -236,6 +255,8 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + fingerprint_id, + session_expiry, } => Self { amount: Some(amount), currency: Some(currency), @@ -255,6 +276,8 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + fingerprint_id, + session_expiry, ..Default::default() }, PaymentIntentUpdate::MetadataUpdate { @@ -283,10 +306,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 +338,7 @@ impl From for PaymentIntentUpdateInternal { // customer_id, return_url, updated_by, + incremental_authorization_allowed, } => Self { // amount, // currency: Some(currency), @@ -319,6 +348,7 @@ impl From for PaymentIntentUpdateInternal { return_url, modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { @@ -369,6 +399,16 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } => Self { + amount: Some(amount), + ..Default::default() + }, + PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + } => Self { + authorization_count: Some(authorization_count), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/Cargo.toml b/crates/diesel_models/Cargo.toml index ccef0bf4e742..35a86f2e85cd 100644 --- a/crates/diesel_models/Cargo.toml +++ b/crates/diesel_models/Cargo.toml @@ -17,8 +17,8 @@ diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time", "64- error-stack = "0.3.1" frunk = "0.4.1" frunk_core = "0.4.1" -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } diff --git a/crates/diesel_models/src/authorization.rs b/crates/diesel_models/src/authorization.rs new file mode 100644 index 000000000000..b6f75bbb9b77 --- /dev/null +++ b/crates/diesel_models/src/authorization.rs @@ -0,0 +1,93 @@ +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{enums as storage_enums, schema::incremental_authorization}; + +#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize, Hash)] +#[diesel(table_name = incremental_authorization)] +#[diesel(primary_key(authorization_id, merchant_id))] +pub struct Authorization { + pub authorization_id: String, + pub merchant_id: String, + pub payment_id: String, + pub amount: i64, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub status: storage_enums::AuthorizationStatus, + pub error_code: Option, + pub error_message: Option, + pub connector_authorization_id: Option, + pub previously_authorized_amount: i64, +} + +#[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize)] +#[diesel(table_name = incremental_authorization)] +pub struct AuthorizationNew { + pub authorization_id: String, + pub merchant_id: String, + pub payment_id: String, + pub amount: i64, + pub status: storage_enums::AuthorizationStatus, + pub error_code: Option, + pub error_message: Option, + pub connector_authorization_id: Option, + pub previously_authorized_amount: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuthorizationUpdate { + StatusUpdate { + status: storage_enums::AuthorizationStatus, + error_code: Option, + error_message: Option, + connector_authorization_id: Option, + }, +} + +#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = incremental_authorization)] +pub struct AuthorizationUpdateInternal { + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub modified_at: Option, + pub connector_authorization_id: Option, +} + +impl AuthorizationUpdateInternal { + pub fn create_authorization(self, source: Authorization) -> Authorization { + Authorization { + status: self.status.unwrap_or(source.status), + error_code: self.error_code.or(source.error_code), + error_message: self.error_message.or(source.error_message), + modified_at: self.modified_at.unwrap_or(common_utils::date_time::now()), + connector_authorization_id: self + .connector_authorization_id + .or(source.connector_authorization_id), + ..source + } + } +} + +impl From for AuthorizationUpdateInternal { + fn from(authorization_child_update: AuthorizationUpdate) -> Self { + let now = Some(common_utils::date_time::now()); + match authorization_child_update { + AuthorizationUpdate::StatusUpdate { + status, + error_code, + error_message, + connector_authorization_id, + } => Self { + status: Some(status), + error_code, + error_message, + connector_authorization_id, + modified_at: now, + }, + } + } +} diff --git a/crates/diesel_models/src/blocklist.rs b/crates/diesel_models/src/blocklist.rs new file mode 100644 index 000000000000..9e88802aa3bb --- /dev/null +++ b/crates/diesel_models/src/blocklist.rs @@ -0,0 +1,26 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::schema::blocklist; + +#[derive(Clone, Debug, Eq, Insertable, PartialEq, Serialize, Deserialize)] +#[diesel(table_name = blocklist)] +pub struct BlocklistNew { + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub metadata: Option, + pub created_at: time::PrimitiveDateTime, +} + +#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Deserialize, Serialize)] +#[diesel(table_name = blocklist)] +pub struct Blocklist { + #[serde(skip)] + pub id: i32, + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub metadata: Option, + pub created_at: time::PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/blocklist_fingerprint.rs b/crates/diesel_models/src/blocklist_fingerprint.rs new file mode 100644 index 000000000000..e75856622e2f --- /dev/null +++ b/crates/diesel_models/src/blocklist_fingerprint.rs @@ -0,0 +1,26 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::schema::blocklist_fingerprint; + +#[derive(Clone, Debug, Eq, Insertable, PartialEq, Serialize, Deserialize)] +#[diesel(table_name = blocklist_fingerprint)] +pub struct BlocklistFingerprintNew { + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub encrypted_fingerprint: String, + pub created_at: time::PrimitiveDateTime, +} + +#[derive(Clone, Debug, Eq, PartialEq, Queryable, Identifiable, Deserialize, Serialize)] +#[diesel(table_name = blocklist_fingerprint)] +pub struct BlocklistFingerprint { + #[serde(skip_serializing)] + pub id: i32, + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub encrypted_fingerprint: String, + pub created_at: time::PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/blocklist_lookup.rs b/crates/diesel_models/src/blocklist_lookup.rs new file mode 100644 index 000000000000..ad2a893e03d9 --- /dev/null +++ b/crates/diesel_models/src/blocklist_lookup.rs @@ -0,0 +1,20 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::schema::blocklist_lookup; + +#[derive(Default, Clone, Debug, Eq, Insertable, PartialEq, Serialize, Deserialize)] +#[diesel(table_name = blocklist_lookup)] +pub struct BlocklistLookupNew { + pub merchant_id: String, + pub fingerprint: String, +} + +#[derive(Default, Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Deserialize, Serialize)] +#[diesel(table_name = blocklist_lookup)] +pub struct BlocklistLookup { + #[serde(skip)] + pub id: i32, + pub merchant_id: String, + pub fingerprint: String, +} diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 1f6c4f604958..ad66eb7f6f16 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -32,6 +32,8 @@ pub struct BusinessProfile { pub is_recon_enabled: bool, #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, + pub payment_link_config: Option, + pub session_expiry: Option, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -55,6 +57,8 @@ pub struct BusinessProfileNew { pub is_recon_enabled: bool, #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, + pub payment_link_config: Option, + pub session_expiry: Option, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -75,6 +79,8 @@ pub struct BusinessProfileUpdateInternal { pub is_recon_enabled: Option, #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, + pub payment_link_config: Option, + pub session_expiry: Option, } impl From for BusinessProfile { @@ -97,31 +103,51 @@ impl From for BusinessProfile { payout_routing_algorithm: new.payout_routing_algorithm, is_recon_enabled: new.is_recon_enabled, applepay_verified_domains: new.applepay_verified_domains, + payment_link_config: new.payment_link_config, + session_expiry: new.session_expiry, } } } 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, + payment_link_config, + session_expiry, + } = 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, + payment_link_config, + session_expiry, ..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..babffdbc4a86 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -2,10 +2,12 @@ pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, - DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, + DbBlocklistDataKind as BlocklistDataKind, DbCaptureMethod as CaptureMethod, + DbCaptureStatus as CaptureStatus, 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, + DbDashboardMetadata as DashboardMetadata, 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 +16,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, - DbRoutingAlgorithmKind as RoutingAlgorithmKind, + DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, + DbRoutingAlgorithmKind as RoutingAlgorithmKind, DbUserStatus as UserStatus, }; } pub use common_enums::*; use common_utils::pii; use diesel::serialize::{Output, ToSql}; +use router_derive::diesel_enum; use time::PrimitiveDateTime; #[derive( @@ -33,7 +37,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 +58,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 +79,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 +100,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 +129,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 +152,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 { @@ -162,6 +166,17 @@ use diesel::{ expression::AsExpression, sql_types::Jsonb, }; + +#[derive( + serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, +)] +#[diesel(sql_type = Jsonb)] +#[serde(rename_all = "snake_case")] +pub struct MandateDetails { + pub update_mandate_id: Option, + pub mandate_type: Option, +} + #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, )] @@ -181,6 +196,7 @@ where Ok(serde_json::from_value(value)?) } } + impl ToSql for MandateDataType where serde_json::Value: ToSql, @@ -195,6 +211,40 @@ where } } +#[derive( + serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, +)] +#[diesel(sql_type = Jsonb)] +#[serde(untagged)] +#[serde(rename_all = "snake_case")] +pub enum MandateTypeDetails { + MandateType(MandateDataType), + MandateDetails(MandateDetails), +} + +impl FromSql for MandateTypeDetails +where + serde_json::Value: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + Ok(serde_json::from_value(value)?) + } +} +impl ToSql for MandateTypeDetails +where + serde_json::Value: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, diesel::pg::Pg>) -> diesel::serialize::Result { + let value = serde_json::to_value(self)?; + + // the function `reborrow` only works in case of `Pg` backend. But, in case of other backends + // please refer to the diesel migration blog: + // https://github.com/Diesel-rs/Diesel/blob/master/guide_drafts/migration_guide.md#changed-tosql-implementations + >::to_sql(&value, &mut out.reborrow()) + } +} + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct MandateAmountData { pub amount: i64, @@ -216,7 +266,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 +397,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 +418,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 +442,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 +465,7 @@ pub enum FraudCheckLastStep { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum UserStatus { @@ -423,3 +473,42 @@ 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 = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DashboardMetadata { + ProductionAgreement, + SetupProcessor, + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected, + SecondProcessorConnected, + ConfiguredRouting, + TestPayment, + IntegrationMethod, + ConfigurationType, + IntegrationCompleted, + StripeConnected, + PaypalConnected, + SpRoutingConfigured, + Feedback, + ProdIntent, + SpTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} diff --git a/crates/diesel_models/src/errors.rs b/crates/diesel_models/src/errors.rs index 0a8422131ae2..4a536aad07e4 100644 --- a/crates/diesel_models/src/errors.rs +++ b/crates/diesel_models/src/errors.rs @@ -1,4 +1,4 @@ -#[derive(Debug, thiserror::Error)] +#[derive(Copy, Clone, Debug, thiserror::Error)] pub enum DatabaseError { #[error("An error occurred when obtaining database connection")] DatabaseConnectionError, @@ -14,3 +14,17 @@ pub enum DatabaseError { #[error("An unknown error occurred")] Others, } + +impl From for DatabaseError { + fn from(error: diesel::result::Error) -> Self { + match error { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::UniqueViolation, + _, + ) => Self::UniqueViolation, + diesel::result::Error::NotFound => Self::NotFound, + diesel::result::Error::QueryBuilderError(_) => Self::QueryGenerationFailed, + _ => Self::Others, + } + } +} 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/kv.rs b/crates/diesel_models/src/kv.rs index f56ef8304186..d067192e2478 100644 --- a/crates/diesel_models/src/kv.rs +++ b/crates/diesel_models/src/kv.rs @@ -7,8 +7,8 @@ use crate::{ payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, payment_intent::{PaymentIntentNew, PaymentIntentUpdate}, refund::{Refund, RefundNew, RefundUpdate}, - reverse_lookup::ReverseLookupNew, - PaymentIntent, + reverse_lookup::{ReverseLookup, ReverseLookupNew}, + PaymentIntent, PgPooledConn, }; #[derive(Debug, Serialize, Deserialize)] @@ -16,7 +16,41 @@ use crate::{ pub enum DBOperation { Insert { insertable: Insertable }, Update { updatable: Updateable }, - Delete, +} + +impl DBOperation { + pub fn operation<'a>(&self) -> &'a str { + match self { + Self::Insert { .. } => "insert", + Self::Update { .. } => "update", + } + } + pub fn table<'a>(&self) -> &'a str { + match self { + Self::Insert { insertable } => match insertable { + Insertable::PaymentIntent(_) => "payment_intent", + Insertable::PaymentAttempt(_) => "payment_attempt", + Insertable::Refund(_) => "refund", + Insertable::Address(_) => "address", + Insertable::ReverseLookUp(_) => "reverse_lookup", + }, + Self::Update { updatable } => match updatable { + Updateable::PaymentIntentUpdate(_) => "payment_intent", + Updateable::PaymentAttemptUpdate(_) => "payment_attempt", + Updateable::RefundUpdate(_) => "refund", + Updateable::AddressUpdate(_) => "address", + }, + } + } +} + +#[derive(Debug)] +pub enum DBResult { + PaymentIntent(Box), + PaymentAttempt(Box), + Refund(Box), + Address(Box
), + ReverseLookUp(Box), } #[derive(Debug, Serialize, Deserialize)] @@ -25,12 +59,48 @@ pub struct TypedSql { pub op: DBOperation, } +impl DBOperation { + pub async fn execute(self, conn: &PgPooledConn) -> crate::StorageResult { + Ok(match self { + Self::Insert { insertable } => match insertable { + Insertable::PaymentIntent(a) => { + DBResult::PaymentIntent(Box::new(a.insert(conn).await?)) + } + Insertable::PaymentAttempt(a) => { + DBResult::PaymentAttempt(Box::new(a.insert(conn).await?)) + } + Insertable::Refund(a) => DBResult::Refund(Box::new(a.insert(conn).await?)), + Insertable::Address(addr) => DBResult::Address(Box::new(addr.insert(conn).await?)), + Insertable::ReverseLookUp(rev) => { + DBResult::ReverseLookUp(Box::new(rev.insert(conn).await?)) + } + }, + Self::Update { updatable } => match updatable { + Updateable::PaymentIntentUpdate(a) => { + DBResult::PaymentIntent(Box::new(a.orig.update(conn, a.update_data).await?)) + } + Updateable::PaymentAttemptUpdate(a) => DBResult::PaymentAttempt(Box::new( + a.orig.update_with_attempt_id(conn, a.update_data).await?, + )), + Updateable::RefundUpdate(a) => { + DBResult::Refund(Box::new(a.orig.update(conn, a.update_data).await?)) + } + Updateable::AddressUpdate(a) => { + DBResult::Address(Box::new(a.orig.update(conn, a.update_data).await?)) + } + }, + }) + } +} + impl TypedSql { pub fn to_field_value_pairs( &self, request_id: String, global_id: String, ) -> crate::StorageResult> { + let pushed_at = common_utils::date_time::now_unix_timestamp(); + Ok(vec![ ( "typed_sql", @@ -40,6 +110,7 @@ impl TypedSql { ), ("global_id", global_id), ("request_id", request_id), + ("pushed_at", pushed_at.to_string()), ]) } } diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 781099662a50..82b1e29ee838 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -1,10 +1,14 @@ pub mod address; pub mod api_keys; +pub mod blocklist_lookup; pub mod business_profile; pub mod capture; pub mod cards_info; pub mod configs; +pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; pub mod customers; pub mod dispute; pub mod encryption; diff --git a/crates/diesel_models/src/mandate.rs b/crates/diesel_models/src/mandate.rs index cc3474914c01..31c6ef62f2b4 100644 --- a/crates/diesel_models/src/mandate.rs +++ b/crates/diesel_models/src/mandate.rs @@ -75,6 +75,12 @@ pub enum MandateUpdate { ConnectorReferenceUpdate { connector_mandate_ids: Option, }, + ConnectorMandateIdUpdate { + connector_mandate_id: Option, + connector_mandate_ids: Option, + payment_method_id: String, + original_payment_id: Option, + }, } #[derive(Clone, Eq, PartialEq, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] @@ -89,6 +95,9 @@ pub struct MandateUpdateInternal { mandate_status: Option, amount_captured: Option, connector_mandate_ids: Option, + connector_mandate_id: Option, + payment_method_id: Option, + original_payment_id: Option, } impl From for MandateUpdateInternal { @@ -98,16 +107,34 @@ impl From for MandateUpdateInternal { mandate_status: Some(mandate_status), connector_mandate_ids: None, amount_captured: None, + connector_mandate_id: None, + payment_method_id: None, + original_payment_id: None, }, MandateUpdate::CaptureAmountUpdate { amount_captured } => Self { mandate_status: None, amount_captured, connector_mandate_ids: None, + connector_mandate_id: None, + payment_method_id: None, + original_payment_id: None, }, MandateUpdate::ConnectorReferenceUpdate { - connector_mandate_ids: connector_mandate_id, + connector_mandate_ids, + } => Self { + connector_mandate_ids, + ..Default::default() + }, + MandateUpdate::ConnectorMandateIdUpdate { + connector_mandate_id, + connector_mandate_ids, + payment_method_id, + original_payment_id, } => Self { - connector_mandate_ids: connector_mandate_id, + connector_mandate_id, + connector_mandate_ids, + payment_method_id: Some(payment_method_id), + original_payment_id, ..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..4a7603384c55 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -51,7 +51,7 @@ pub struct PaymentAttempt { pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, // providing a location to store mandate details intermediately for transaction - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub multiple_capture_count: Option, // reference to the payment at connector side @@ -61,6 +61,17 @@ pub struct PaymentAttempt { pub merchant_connector_id: Option, pub authentication_data: Option, pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, + pub net_amount: Option, +} + +impl PaymentAttempt { + pub fn get_or_calculate_net_amount(&self) -> i64 { + self.net_amount.unwrap_or( + self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0), + ) + } } #[derive(Clone, Debug, Eq, PartialEq, Queryable, Serialize, Deserialize)] @@ -115,7 +126,7 @@ pub struct PaymentAttemptNew { pub business_sub_label: Option, pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub connector_response_reference_id: Option, pub multiple_capture_count: Option, @@ -124,6 +135,27 @@ pub struct PaymentAttemptNew { pub merchant_connector_id: Option, pub authentication_data: Option, pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, + pub net_amount: Option, +} + +impl PaymentAttemptNew { + /// returns amount + surcharge_amount + tax_amount + pub fn calculate_net_amount(&self) -> i64 { + self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0) + } + + pub fn get_or_calculate_net_amount(&self) -> i64 { + self.net_amount + .unwrap_or_else(|| self.calculate_net_amount()) + } + + pub fn populate_derived_fields(self) -> Self { + let mut payment_attempt_new = self; + payment_attempt_new.net_amount = Some(payment_attempt_new.calculate_net_amount()); + payment_attempt_new + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -176,6 +208,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 +238,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 +267,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 { @@ -259,12 +297,17 @@ pub enum PaymentAttemptUpdate { connector: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + amount_capturable: i64, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] #[diesel(table_name = payment_attempt)] pub struct PaymentAttemptUpdateInternal { amount: Option, + net_amount: Option, currency: Option, status: Option, connector_transaction_id: Option, @@ -298,60 +341,109 @@ pub struct PaymentAttemptUpdateInternal { merchant_connector_id: Option, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, +} + +impl PaymentAttemptUpdateInternal { + pub fn populate_derived_fields(self, source: &PaymentAttempt) -> Self { + let mut update_internal = self; + update_internal.net_amount = Some( + update_internal.amount.unwrap_or(source.amount) + + update_internal + .surcharge_amount + .or(source.surcharge_amount) + .unwrap_or(0) + + update_internal + .tax_amount + .or(source.tax_amount) + .unwrap_or(0), + ); + update_internal + } } impl PaymentAttemptUpdate { pub fn apply_changeset(self, source: PaymentAttempt) -> PaymentAttempt { - let pa_update: PaymentAttemptUpdateInternal = self.into(); + let PaymentAttemptUpdateInternal { + amount, + net_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, + } = PaymentAttemptUpdateInternal::from(self).populate_derived_fields(&source); 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), + net_amount: net_amount.or(source.net_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 +517,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 +539,8 @@ impl From for PaymentAttemptUpdateInternal { amount_capturable, updated_by, merchant_connector_id, + surcharge_amount, + tax_amount, ..Default::default() }, PaymentAttemptUpdate::VoidUpdate { @@ -483,11 +579,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 +600,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 +614,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 +626,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 +698,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 { @@ -628,6 +732,14 @@ impl From for PaymentAttemptUpdateInternal { updated_by, ..Default::default() }, + PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + } => Self { + amount: Some(amount), + amount_capturable: Some(amount_capturable), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 2ffa857026ba..31bc0c06c51d 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,18 +52,15 @@ pub struct PaymentIntent { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: Option, + pub incremental_authorization_allowed: Option, + pub authorization_count: Option, + pub session_expiry: Option, + pub fingerprint_id: Option, } #[derive( - Clone, - Debug, - Default, - Eq, - PartialEq, - Insertable, - router_derive::DebugAsDisplay, - Serialize, - Deserialize, + Clone, Debug, Eq, PartialEq, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize, )] #[diesel(table_name = payment_intent)] pub struct PaymentIntentNew { @@ -103,9 +101,14 @@ pub struct PaymentIntentNew { pub merchant_decision: Option, pub payment_link_id: Option, pub payment_confirm_source: Option, - pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: Option, + pub incremental_authorization_allowed: Option, + pub authorization_count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub session_expiry: Option, + pub fingerprint_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -115,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 +141,7 @@ pub enum PaymentIntentUpdate { PGStatusUpdate { status: storage_enums::IntentStatus, updated_by: String, + incremental_authorization_allowed: Option, }, Update { amount: i64, @@ -156,6 +161,8 @@ pub enum PaymentIntentUpdate { metadata: Option, payment_confirm_source: Option, updated_by: String, + session_expiry: Option, + fingerprint_id: Option, }, PaymentAttemptAndAttemptCountUpdate { active_attempt_id: String, @@ -181,6 +188,12 @@ pub enum PaymentIntentUpdate { surcharge_applicable: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + }, + AuthorizationCountUpdate { + authorization_count: i32, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -213,54 +226,79 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, + pub incremental_authorization_allowed: Option, + pub authorization_count: Option, + pub session_expiry: Option, + pub fingerprint_id: 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, + authorization_count, + session_expiry, + fingerprint_id, + } = 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: incremental_authorization_allowed + .or(source.incremental_authorization_allowed), + authorization_count: authorization_count.or(source.authorization_count), + fingerprint_id: fingerprint_id.or(source.fingerprint_id), + session_expiry: session_expiry.or(source.session_expiry), ..source } } @@ -287,6 +325,8 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + session_expiry, + fingerprint_id, } => Self { amount: Some(amount), currency: Some(currency), @@ -306,6 +346,8 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + session_expiry, + fingerprint_id, ..Default::default() }, PaymentIntentUpdate::MetadataUpdate { @@ -334,10 +376,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 +408,7 @@ impl From for PaymentIntentUpdateInternal { // customer_id, return_url, updated_by, + incremental_authorization_allowed, } => Self { // amount, // currency: Some(currency), @@ -370,6 +418,7 @@ impl From for PaymentIntentUpdateInternal { return_url, modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { @@ -420,6 +469,16 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } => Self { + amount: Some(amount), + ..Default::default() + }, + PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + } => Self { + authorization_count: Some(authorization_count), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs index 50cc5e89cee9..ed0e979d0268 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 { @@ -18,14 +18,17 @@ pub struct PaymentLink { 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")] + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, pub custom_merchant_name: Option, + pub payment_link_config: Option, + pub description: Option, + pub profile_id: Option, } + #[derive( Clone, Debug, - Default, Eq, PartialEq, Insertable, @@ -45,7 +48,10 @@ pub struct PaymentLinkNew { pub created_at: Option, #[serde(with = "common_utils::custom_serde::iso8601::option")] pub last_modified_at: Option, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, pub custom_merchant_name: Option, + pub payment_link_config: Option, + pub description: Option, + pub profile_id: 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..3a0a008b76bd 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -1,11 +1,16 @@ pub mod address; pub mod api_keys; +pub mod blocklist_lookup; pub mod business_profile; mod capture; pub mod cards_info; pub mod configs; +pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod events; pub mod file; diff --git a/crates/diesel_models/src/query/authorization.rs b/crates/diesel_models/src/query/authorization.rs new file mode 100644 index 000000000000..dc9515bda55e --- /dev/null +++ b/crates/diesel_models/src/query/authorization.rs @@ -0,0 +1,79 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + authorization::{ + Authorization, AuthorizationNew, AuthorizationUpdate, AuthorizationUpdateInternal, + }, + errors, + schema::incremental_authorization::dsl, + PgPooledConn, StorageResult, +}; + +impl AuthorizationNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Authorization { + #[instrument(skip(conn))] + pub async fn update_by_merchant_id_authorization_id( + conn: &PgPooledConn, + merchant_id: String, + authorization_id: String, + authorization_update: AuthorizationUpdate, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authorization_id.eq(authorization_id.to_owned())), + AuthorizationUpdateInternal::from(authorization_update), + ) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NotFound => Err(error.attach_printable( + "Authorization with the given Authorization ID does not exist", + )), + errors::DatabaseError::NoFieldsToUpdate => { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authorization_id.eq(authorization_id.to_owned())), + ) + .await + } + _ => Err(error), + }, + result => result, + } + } + + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_payment_id( + conn: &PgPooledConn, + merchant_id: &str, + payment_id: &str, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::payment_id.eq(payment_id.to_owned())), + None, + None, + Some(dsl::created_at.asc()), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/blocklist.rs b/crates/diesel_models/src/query/blocklist.rs new file mode 100644 index 000000000000..e1ba5fa923d6 --- /dev/null +++ b/crates/diesel_models/src/query/blocklist.rs @@ -0,0 +1,83 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + blocklist::{Blocklist, BlocklistNew}, + schema::blocklist::dsl, + PgPooledConn, StorageResult, +}; + +impl BlocklistNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Blocklist { + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_fingerprint_id( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint_id.eq(fingerprint_id.to_owned())), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn list_by_merchant_id_data_kind( + conn: &PgPooledConn, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::data_kind.eq(data_kind.to_owned())), + Some(limit), + Some(offset), + Some(dsl::created_at.desc()), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn list_by_merchant_id( + conn: &PgPooledConn, + merchant_id: &str, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id.eq(merchant_id.to_owned()), + None, + None, + Some(dsl::created_at.desc()), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn delete_by_merchant_id_fingerprint_id( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint_id: &str, + ) -> StorageResult { + generics::generic_delete_one_with_result::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint_id.eq(fingerprint_id.to_owned())), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/blocklist_fingerprint.rs b/crates/diesel_models/src/query/blocklist_fingerprint.rs new file mode 100644 index 000000000000..4f3d77e63a81 --- /dev/null +++ b/crates/diesel_models/src/query/blocklist_fingerprint.rs @@ -0,0 +1,33 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + blocklist_fingerprint::{BlocklistFingerprint, BlocklistFingerprintNew}, + schema::blocklist_fingerprint::dsl, + PgPooledConn, StorageResult, +}; + +impl BlocklistFingerprintNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl BlocklistFingerprint { + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_fingerprint_id( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint_id.eq(fingerprint_id.to_owned())), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/blocklist_lookup.rs b/crates/diesel_models/src/query/blocklist_lookup.rs new file mode 100644 index 000000000000..ea28c94e4916 --- /dev/null +++ b/crates/diesel_models/src/query/blocklist_lookup.rs @@ -0,0 +1,48 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + blocklist_lookup::{BlocklistLookup, BlocklistLookupNew}, + schema::blocklist_lookup::dsl, + PgPooledConn, StorageResult, +}; + +impl BlocklistLookupNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl BlocklistLookup { + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_fingerprint( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint.eq(fingerprint.to_owned())), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn delete_by_merchant_id_fingerprint( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint: &str, + ) -> StorageResult { + generics::generic_delete_one_with_result::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint.eq(fingerprint.to_owned())), + ) + .await + } +} 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..b1cb034eb1f6 --- /dev/null +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -0,0 +1,121 @@ +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, DashboardMetadataUpdate, + DashboardMetadataUpdateInternal, + }, + 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 update( + conn: &PgPooledConn, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: DashboardMetadataUpdate, + ) -> StorageResult { + let predicate = dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::org_id.eq(org_id.to_owned())) + .and(dsl::data_key.eq(data_key.to_owned())); + + if let Some(uid) = user_id { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + predicate.and(dsl::user_id.eq(uid)), + DashboardMetadataUpdateInternal::from(dashboard_metadata_update), + ) + .await + } else { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + predicate.and(dsl::user_id.is_null()), + DashboardMetadataUpdateInternal::from(dashboard_metadata_update), + ) + .await + } + } + + 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 + } + + pub async fn delete_user_scoped_dashboard_metadata_by_merchant_id( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + ) -> StorageResult { + generics::generic_delete::<::Table, _>( + conn, + dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/merchant_account.rs b/crates/diesel_models/src/query/merchant_account.rs index ef9a4165d6f4..ba20f2e36607 100644 --- a/crates/diesel_models/src/query/merchant_account.rs +++ b/crates/diesel_models/src/query/merchant_account.rs @@ -110,4 +110,24 @@ impl MerchantAccount { ) .await } + + #[instrument(skip_all)] + pub async fn list_multiple_merchant_accounts( + conn: &PgPooledConn, + merchant_ids: Vec, + ) -> StorageResult> { + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + _, + >( + conn, + dsl::merchant_id.eq_any(merchant_ids), + None, + None, + None, + ) + .await + } } diff --git a/crates/diesel_models/src/query/merchant_key_store.rs b/crates/diesel_models/src/query/merchant_key_store.rs index 27ec3be9fcd0..0e2ec1ddadbd 100644 --- a/crates/diesel_models/src/query/merchant_key_store.rs +++ b/crates/diesel_models/src/query/merchant_key_store.rs @@ -39,4 +39,24 @@ impl MerchantKeyStore { ) .await } + + #[instrument(skip(conn))] + pub async fn list_multiple_key_stores( + conn: &PgPooledConn, + merchant_ids: Vec, + ) -> StorageResult> { + generics::generic_filter::< + ::Table, + _, + <::Table as diesel::Table>::PrimaryKey, + _, + >( + conn, + dsl::merchant_id.eq_any(merchant_ids), + None, + None, + None, + ) + .await + } } diff --git a/crates/diesel_models/src/query/payment_attempt.rs b/crates/diesel_models/src/query/payment_attempt.rs index 4737233e3048..ebd0dcd7351b 100644 --- a/crates/diesel_models/src/query/payment_attempt.rs +++ b/crates/diesel_models/src/query/payment_attempt.rs @@ -23,7 +23,7 @@ use crate::{ impl PaymentAttemptNew { #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { - generics::generic_insert(conn, self).await + generics::generic_insert(conn, self.populate_derived_fields()).await } } @@ -44,7 +44,7 @@ impl PaymentAttempt { dsl::attempt_id .eq(self.attempt_id.to_owned()) .and(dsl::merchant_id.eq(self.merchant_id.to_owned())), - PaymentAttemptUpdateInternal::from(payment_attempt), + PaymentAttemptUpdateInternal::from(payment_attempt).populate_derived_fields(&self), ) .await { @@ -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/query/user.rs b/crates/diesel_models/src/query/user.rs index 5761d8af814d..6fb5b79ddc1e 100644 --- a/crates/diesel_models/src/query/user.rs +++ b/crates/diesel_models/src/query/user.rs @@ -1,12 +1,24 @@ -use diesel::{associations::HasTable, ExpressionMethods}; -use error_stack::report; -use router_env::tracing::{self, instrument}; +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{ + associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods, + JoinOnDsl, QueryDsl, +}; +use error_stack::IntoReport; +use router_env::{ + logger, + tracing::{self, instrument}, +}; +pub mod sample_data; use crate::{ errors::{self}, query::generics, - schema::users::dsl, + schema::{ + user_roles::{self, dsl as user_roles_dsl}, + users::dsl as users_dsl, + }, user::*, + user_role::UserRole, PgPooledConn, StorageResult, }; @@ -21,7 +33,7 @@ impl User { pub async fn find_by_user_email(conn: &PgPooledConn, user_email: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, - dsl::email.eq(user_email.to_owned()), + users_dsl::email.eq(user_email.to_owned()), ) .await } @@ -29,7 +41,7 @@ impl User { pub async fn find_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, - dsl::user_id.eq(user_id.to_owned()), + users_dsl::user_id.eq(user_id.to_owned()), ) .await } @@ -37,26 +49,64 @@ impl User { pub async fn update_by_user_id( conn: &PgPooledConn, user_id: &str, - user: UserUpdate, + user_update: UserUpdate, ) -> StorageResult { - generics::generic_update_with_results::<::Table, _, _, _>( + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( conn, - dsl::user_id.eq(user_id.to_owned()), - UserUpdateInternal::from(user), + users_dsl::user_id.eq(user_id.to_owned()), + UserUpdateInternal::from(user_update), ) - .await? - .first() - .cloned() - .ok_or_else(|| { - report!(errors::DatabaseError::NotFound).attach_printable("Error while updating user") - }) + .await + } + + pub async fn update_by_user_email( + conn: &PgPooledConn, + user_email: &str, + user_update: UserUpdate, + ) -> StorageResult { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + users_dsl::email.eq(user_email.to_owned()), + UserUpdateInternal::from(user_update), + ) + .await } pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { generics::generic_delete::<::Table, _>( conn, - dsl::user_id.eq(user_id.to_owned()), + users_dsl::user_id.eq(user_id.to_owned()), ) .await } + + pub async fn find_joined_users_and_roles_by_merchant_id( + conn: &PgPooledConn, + mid: &str, + ) -> StorageResult> { + let query = Self::table() + .inner_join(user_roles::table.on(user_roles_dsl::user_id.eq(users_dsl::user_id))) + .filter(user_roles_dsl::merchant_id.eq(mid.to_owned())); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async::<(Self, UserRole)>(conn) + .await + .into_report() + .map_err(|err| match err.current_context() { + DieselError::NotFound => err.change_context(errors::DatabaseError::NotFound), + _ => err.change_context(errors::DatabaseError::Others), + }) + } } diff --git a/crates/diesel_models/src/query/user/sample_data.rs b/crates/diesel_models/src/query/user/sample_data.rs new file mode 100644 index 000000000000..a8ec2c3b0a4f --- /dev/null +++ b/crates/diesel_models/src/query/user/sample_data.rs @@ -0,0 +1,139 @@ +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{associations::HasTable, debug_query, ExpressionMethods, TextExpressionMethods}; +use error_stack::{IntoReport, ResultExt}; +use router_env::logger; + +use crate::{ + errors, + schema::{ + payment_attempt::dsl as payment_attempt_dsl, payment_intent::dsl as payment_intent_dsl, + refund::dsl as refund_dsl, + }, + user::sample_data::PaymentAttemptBatchNew, + PaymentAttempt, PaymentIntent, PaymentIntentNew, PgPooledConn, Refund, RefundNew, + StorageResult, +}; + +pub async fn insert_payment_intents( + conn: &PgPooledConn, + batch: Vec, +) -> StorageResult> { + let query = diesel::insert_into(::table()).values(batch); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while inserting payment intents") +} +pub async fn insert_payment_attempts( + conn: &PgPooledConn, + batch: Vec, +) -> StorageResult> { + let query = diesel::insert_into(::table()).values(batch); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while inserting payment attempts") +} + +pub async fn insert_refunds( + conn: &PgPooledConn, + batch: Vec, +) -> StorageResult> { + let query = diesel::insert_into(::table()).values(batch); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while inserting refunds") +} + +pub async fn delete_payment_intents( + conn: &PgPooledConn, + merchant_id: &str, +) -> StorageResult> { + let query = diesel::delete(::table()) + .filter(payment_intent_dsl::merchant_id.eq(merchant_id.to_owned())) + .filter(payment_intent_dsl::payment_id.like("test_%")); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while deleting payment intents") + .and_then(|result| match result.len() { + n if n > 0 => { + logger::debug!("{n} records deleted"); + Ok(result) + } + 0 => Err(error_stack::report!(errors::DatabaseError::NotFound) + .attach_printable("No records deleted")), + _ => Ok(result), + }) +} +pub async fn delete_payment_attempts( + conn: &PgPooledConn, + merchant_id: &str, +) -> StorageResult> { + let query = diesel::delete(::table()) + .filter(payment_attempt_dsl::merchant_id.eq(merchant_id.to_owned())) + .filter(payment_attempt_dsl::payment_id.like("test_%")); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while deleting payment attempts") + .and_then(|result| match result.len() { + n if n > 0 => { + logger::debug!("{n} records deleted"); + Ok(result) + } + 0 => Err(error_stack::report!(errors::DatabaseError::NotFound) + .attach_printable("No records deleted")), + _ => Ok(result), + }) +} + +pub async fn delete_refunds(conn: &PgPooledConn, merchant_id: &str) -> StorageResult> { + let query = diesel::delete(::table()) + .filter(refund_dsl::merchant_id.eq(merchant_id.to_owned())) + .filter(refund_dsl::payment_id.like("test_%")); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while deleting refunds") + .and_then(|result| match result.len() { + n if n > 0 => { + logger::debug!("{n} records deleted"); + Ok(result) + } + 0 => Err(error_stack::report!(errors::DatabaseError::NotFound) + .attach_printable("No records deleted")), + _ => Ok(result), + }) +} diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs index d2f9564a5309..e67eba64c7cd 100644 --- a/crates/diesel_models/src/query/user_role.rs +++ b/crates/diesel_models/src/query/user_role.rs @@ -19,6 +19,20 @@ impl UserRole { .await } + pub async fn find_by_user_id_merchant_id( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)), + ) + .await + } + pub async fn update_by_user_id_merchant_id( conn: &PgPooledConn, user_id: String, @@ -40,9 +54,18 @@ impl UserRole { .await } - pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult { - generics::generic_delete::<::Table, _>(conn, dsl::user_id.eq(user_id)) - .await + pub async fn delete_by_user_id_merchant_id( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + ) -> StorageResult { + generics::generic_delete::<::Table, _>( + conn, + dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)), + ) + .await } pub async fn list_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult> { 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..c9887e1770fc 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -57,6 +57,50 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + blocklist (id) { + id -> Int4, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + fingerprint_id -> Varchar, + data_kind -> BlocklistDataKind, + metadata -> Nullable, + created_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + blocklist_fingerprint (id) { + id -> Int4, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + fingerprint_id -> Varchar, + data_kind -> BlocklistDataKind, + encrypted_fingerprint -> Text, + created_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + blocklist_lookup (id) { + id -> Int4, + #[max_length = 64] + merchant_id -> Varchar, + fingerprint -> Text, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -83,6 +127,8 @@ diesel::table! { payout_routing_algorithm -> Nullable, is_recon_enabled -> Bool, applepay_verified_domains -> Nullable>>, + payment_link_config -> Nullable, + session_expiry -> Nullable, } } @@ -183,6 +229,29 @@ 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, + data_key -> DashboardMetadata, + 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 +400,35 @@ diesel::table! { created_at -> Timestamp, last_modified -> Timestamp, step_up_possible -> Bool, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, + } +} + +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + incremental_authorization (authorization_id, merchant_id) { + #[max_length = 64] + authorization_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + payment_id -> Varchar, + amount -> Int8, + created_at -> Timestamp, + modified_at -> Timestamp, + #[max_length = 64] + status -> Varchar, + #[max_length = 255] + error_code -> Nullable, + error_message -> Nullable, + #[max_length = 64] + connector_authorization_id -> Nullable, + previously_authorized_amount -> Int8, } } @@ -492,6 +590,7 @@ diesel::table! { profile_id -> Nullable, applepay_verified_domains -> Nullable>>, pm_auth_config -> Nullable, + status -> ConnectorStatus, } } @@ -584,6 +683,11 @@ diesel::table! { merchant_connector_id -> Nullable, authentication_data -> Nullable, encoded_data -> Nullable, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, + net_amount -> Nullable, } } @@ -645,6 +749,12 @@ diesel::table! { #[max_length = 32] updated_by -> Varchar, surcharge_applicable -> Nullable, + request_incremental_authorization -> Nullable, + incremental_authorization_allowed -> Nullable, + authorization_count -> Nullable, + session_expiry -> Nullable, + #[max_length = 64] + fingerprint_id -> Nullable, } } @@ -668,6 +778,11 @@ diesel::table! { fulfilment_time -> Nullable, #[max_length = 64] custom_merchant_name -> Nullable, + payment_link_config -> Nullable, + #[max_length = 255] + description -> Nullable, + #[max_length = 64] + profile_id -> Nullable, } } @@ -745,7 +860,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, } @@ -914,14 +1029,13 @@ diesel::table! { role_id -> Varchar, #[max_length = 64] org_id -> Varchar, - #[max_length = 64] - status -> Varchar, + status -> UserStatus, #[max_length = 64] created_by -> Varchar, #[max_length = 64] last_modified_by -> Varchar, created_at -> Timestamp, - last_modified_at -> Timestamp, + last_modified -> Timestamp, } } @@ -942,22 +1056,29 @@ diesel::table! { is_verified -> Bool, created_at -> Timestamp, last_modified_at -> Timestamp, + #[max_length = 64] + preferred_merchant_id -> Nullable, } } diesel::allow_tables_to_appear_in_same_query!( address, api_keys, + blocklist, + blocklist_fingerprint, + blocklist_lookup, business_profile, captures, cards_info, configs, customers, + dashboard_metadata, dispute, events, file_metadata, fraud_check, gateway_status_map, + incremental_authorization, locker_mock_up, mandate, merchant_account, diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs index 6a2e864b291c..84fe8710060e 100644 --- a/crates/diesel_models/src/user.rs +++ b/crates/diesel_models/src/user.rs @@ -5,6 +5,9 @@ use time::PrimitiveDateTime; use crate::schema::users; +pub mod dashboard_metadata; + +pub mod sample_data; #[derive(Clone, Debug, Identifiable, Queryable)] #[diesel(table_name = users)] pub struct User { @@ -16,6 +19,7 @@ pub struct User { pub is_verified: bool, pub created_at: PrimitiveDateTime, pub last_modified_at: PrimitiveDateTime, + pub preferred_merchant_id: Option, } #[derive( @@ -30,6 +34,7 @@ pub struct UserNew { pub is_verified: bool, pub created_at: Option, pub last_modified_at: Option, + pub preferred_merchant_id: Option, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -39,6 +44,7 @@ pub struct UserUpdateInternal { password: Option>, is_verified: Option, last_modified_at: PrimitiveDateTime, + preferred_merchant_id: Option, } #[derive(Debug)] @@ -48,6 +54,7 @@ pub enum UserUpdate { name: Option, password: Option>, is_verified: Option, + preferred_merchant_id: Option, }, } @@ -60,16 +67,19 @@ impl From for UserUpdateInternal { password: None, is_verified: Some(true), last_modified_at, + preferred_merchant_id: None, }, UserUpdate::AccountUpdate { name, password, is_verified, + preferred_merchant_id, } => Self { name, password, is_verified, last_modified_at, + preferred_merchant_id, }, } } 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..1eeb61d6135e --- /dev/null +++ b/crates/diesel_models/src/user/dashboard_metadata.rs @@ -0,0 +1,72 @@ +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, +} + +#[derive( + router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay, AsChangeset, +)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadataUpdateInternal { + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} + +pub enum DashboardMetadataUpdate { + UpdateData { + data_key: enums::DashboardMetadata, + data_value: serde_json::Value, + last_modified_by: String, + }, +} + +impl From for DashboardMetadataUpdateInternal { + fn from(metadata_update: DashboardMetadataUpdate) -> Self { + let last_modified_at = common_utils::date_time::now(); + match metadata_update { + DashboardMetadataUpdate::UpdateData { + data_key, + data_value, + last_modified_by, + } => Self { + data_key, + data_value, + last_modified_by, + last_modified_at, + }, + } + } +} diff --git a/crates/diesel_models/src/user/sample_data.rs b/crates/diesel_models/src/user/sample_data.rs new file mode 100644 index 000000000000..5a2226f06764 --- /dev/null +++ b/crates/diesel_models/src/user/sample_data.rs @@ -0,0 +1,121 @@ +use common_enums::{ + AttemptStatus, AuthenticationType, CaptureMethod, Currency, PaymentExperience, PaymentMethod, + PaymentMethodType, +}; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{enums::MandateTypeDetails, schema::payment_attempt, PaymentAttemptNew}; + +#[derive( + Clone, Debug, Default, diesel::Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize, +)] +#[diesel(table_name = payment_attempt)] +pub struct PaymentAttemptBatchNew { + pub payment_id: String, + pub merchant_id: String, + pub attempt_id: String, + pub status: AttemptStatus, + pub amount: i64, + pub currency: Option, + pub save_to_locker: Option, + pub connector: Option, + pub error_message: Option, + pub offer_amount: Option, + pub surcharge_amount: Option, + pub tax_amount: Option, + pub payment_method_id: Option, + pub payment_method: Option, + pub capture_method: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub capture_on: Option, + pub confirm: bool, + pub authentication_type: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub created_at: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub modified_at: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub last_synced: Option, + pub cancellation_reason: Option, + pub amount_to_capture: Option, + pub mandate_id: Option, + pub browser_info: Option, + pub payment_token: Option, + pub error_code: Option, + pub connector_metadata: Option, + pub payment_experience: Option, + pub payment_method_type: Option, + pub payment_method_data: Option, + pub business_sub_label: Option, + pub straight_through_algorithm: Option, + pub preprocessing_step_id: Option, + pub mandate_details: Option, + pub error_reason: Option, + pub connector_response_reference_id: Option, + pub connector_transaction_id: Option, + pub multiple_capture_count: Option, + pub amount_capturable: i64, + pub updated_by: String, + pub merchant_connector_id: Option, + pub authentication_data: Option, + pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, + pub net_amount: Option, +} + +#[allow(dead_code)] +impl PaymentAttemptBatchNew { + // Used to verify compatibility with PaymentAttemptTable + fn convert_into_normal_attempt_insert(self) -> PaymentAttemptNew { + PaymentAttemptNew { + payment_id: self.payment_id, + merchant_id: self.merchant_id, + attempt_id: self.attempt_id, + status: self.status, + amount: self.amount, + currency: self.currency, + save_to_locker: self.save_to_locker, + connector: self.connector, + error_message: self.error_message, + offer_amount: self.offer_amount, + surcharge_amount: self.surcharge_amount, + tax_amount: self.tax_amount, + payment_method_id: self.payment_method_id, + payment_method: self.payment_method, + capture_method: self.capture_method, + capture_on: self.capture_on, + confirm: self.confirm, + authentication_type: self.authentication_type, + created_at: self.created_at, + modified_at: self.modified_at, + last_synced: self.last_synced, + cancellation_reason: self.cancellation_reason, + amount_to_capture: self.amount_to_capture, + mandate_id: self.mandate_id, + browser_info: self.browser_info, + payment_token: self.payment_token, + error_code: self.error_code, + connector_metadata: self.connector_metadata, + payment_experience: self.payment_experience, + payment_method_type: self.payment_method_type, + payment_method_data: self.payment_method_data, + business_sub_label: self.business_sub_label, + straight_through_algorithm: self.straight_through_algorithm, + preprocessing_step_id: self.preprocessing_step_id, + mandate_details: self.mandate_details, + error_reason: self.error_reason, + multiple_capture_count: self.multiple_capture_count, + connector_response_reference_id: self.connector_response_reference_id, + amount_capturable: self.amount_capturable, + updated_by: self.updated_by, + merchant_connector_id: self.merchant_connector_id, + authentication_data: self.authentication_data, + encoded_data: self.encoded_data, + unified_code: self.unified_code, + unified_message: self.unified_message, + net_amount: self.net_amount, + } + } +} diff --git a/crates/diesel_models/src/user_role.rs b/crates/diesel_models/src/user_role.rs index 467584ac59db..3c32092fa917 100644 --- a/crates/diesel_models/src/user_role.rs +++ b/crates/diesel_models/src/user_role.rs @@ -15,7 +15,7 @@ pub struct UserRole { pub created_by: String, pub last_modified_by: String, pub created_at: PrimitiveDateTime, - pub last_modified_at: PrimitiveDateTime, + pub last_modified: PrimitiveDateTime, } #[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -29,7 +29,7 @@ pub struct UserRoleNew { pub created_by: String, pub last_modified_by: String, pub created_at: PrimitiveDateTime, - pub last_modified_at: PrimitiveDateTime, + pub last_modified: PrimitiveDateTime, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -38,7 +38,7 @@ pub struct UserRoleUpdateInternal { role_id: Option, status: Option, last_modified_by: Option, - last_modified_at: PrimitiveDateTime, + last_modified: PrimitiveDateTime, } pub enum UserRoleUpdate { @@ -54,7 +54,7 @@ pub enum UserRoleUpdate { impl From for UserRoleUpdateInternal { fn from(value: UserRoleUpdate) -> Self { - let last_modified_at = common_utils::date_time::now(); + let last_modified = common_utils::date_time::now(); match value { UserRoleUpdate::UpdateRole { role_id, @@ -63,14 +63,14 @@ impl From for UserRoleUpdateInternal { role_id: Some(role_id), last_modified_by: Some(modified_by), status: None, - last_modified_at, + last_modified, }, UserRoleUpdate::UpdateStatus { status, modified_by, } => Self { status: Some(status), - last_modified_at, + last_modified, last_modified_by: Some(modified_by), role_id: None, }, diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index 668e8b0574fe..67169a151044 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [features] release = ["kms", "vergen"] kms = ["external_services/kms"] +hashicorp-vault = ["external_services/hashicorp-vault"] vergen = ["router_env/vergen"] [dependencies] @@ -20,11 +21,12 @@ config = { version = "0.13.3", features = ["toml"] } diesel = { version = "2.1.0", features = ["postgres"] } error-stack = "0.3.1" once_cell = "1.18.0" -serde = "1.0.163" -serde_json = "1.0.96" -serde_path_to_error = "0.1.11" +serde = "1.0.193" +serde_json = "1.0.108" +serde_path_to_error = "0.1.14" thiserror = "1.0.40" -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } +async-trait = "0.1.74" # First Party Crates common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals"] } diff --git a/crates/drainer/src/connection.rs b/crates/drainer/src/connection.rs index 7b273244cbce..6af0a9782232 100644 --- a/crates/drainer/src/connection.rs +++ b/crates/drainer/src/connection.rs @@ -1,5 +1,7 @@ use bb8::PooledConnection; use diesel::PgConnection; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::{self, decrypt::VaultFetch, Kv2}; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; #[cfg(not(feature = "kms"))] @@ -27,16 +29,23 @@ pub async fn diesel_make_pg_pool( database: &Database, _test_transaction: bool, #[cfg(feature = "kms")] kms_client: &'static kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] hashicorp_client: &'static hashicorp_vault::HashiCorpVault, ) -> PgPool { + let password = database.password.clone(); + #[cfg(feature = "hashicorp-vault")] + let password = password + .fetch_inner::(hashicorp_client) + .await + .expect("Failed while fetching db password"); + #[cfg(feature = "kms")] - let password = database - .password + let password = password .decrypt_inner(kms_client) .await .expect("Failed to decrypt password"); #[cfg(not(feature = "kms"))] - let password = &database.password.peek(); + let password = &password.peek(); let database_url = format!( "postgres://{}:{}@{}:{}/{}", diff --git a/crates/drainer/src/errors.rs b/crates/drainer/src/errors.rs index 42ccfc7a0d81..3034e849f8a9 100644 --- a/crates/drainer/src/errors.rs +++ b/crates/drainer/src/errors.rs @@ -11,6 +11,8 @@ pub enum DrainerError { ConfigurationError(config::ConfigError), #[error("Error while configuring signals: {0}")] SignalError(String), + #[error("Error while parsing data from the stream: {0:?}")] + ParsingError(error_stack::Report), #[error("Unexpected error occurred: {0}")] UnexpectedError(String), } diff --git a/crates/drainer/src/handler.rs b/crates/drainer/src/handler.rs new file mode 100644 index 000000000000..5aa902d84c5a --- /dev/null +++ b/crates/drainer/src/handler.rs @@ -0,0 +1,277 @@ +use std::sync::{atomic, Arc}; + +use tokio::{ + sync::{mpsc, oneshot}, + time::{self, Duration}, +}; + +use crate::{ + errors, instrument, logger, metrics, query::ExecuteQuery, tracing, utils, DrainerSettings, + Store, StreamData, +}; + +/// Handler handles the spawning and closing of drainer +/// Arc is used to enable creating a listener for graceful shutdown +#[derive(Clone)] +pub struct Handler { + inner: Arc, +} + +impl std::ops::Deref for Handler { + type Target = HandlerInner; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +pub struct HandlerInner { + shutdown_interval: Duration, + loop_interval: Duration, + active_tasks: Arc, + conf: DrainerSettings, + store: Arc, + running: Arc, +} + +impl Handler { + pub fn from_conf(conf: DrainerSettings, store: Arc) -> Self { + let shutdown_interval = Duration::from_millis(conf.shutdown_interval.into()); + let loop_interval = Duration::from_millis(conf.loop_interval.into()); + + let active_tasks = Arc::new(atomic::AtomicU64::new(0)); + + let running = Arc::new(atomic::AtomicBool::new(true)); + + let handler = HandlerInner { + shutdown_interval, + loop_interval, + active_tasks, + conf, + store, + running, + }; + + Self { + inner: Arc::new(handler), + } + } + + pub fn close(&self) { + self.running.store(false, atomic::Ordering::SeqCst); + } + + pub async fn spawn(&self) -> errors::DrainerResult<()> { + let mut stream_index: u8 = 0; + let jobs_picked = Arc::new(atomic::AtomicU8::new(0)); + + while self.running.load(atomic::Ordering::SeqCst) { + metrics::DRAINER_HEALTH.add(&metrics::CONTEXT, 1, &[]); + if self.store.is_stream_available(stream_index).await { + tokio::spawn(drainer_handler( + self.store.clone(), + stream_index, + self.conf.max_read_count, + self.active_tasks.clone(), + jobs_picked.clone(), + )); + } + stream_index = utils::increment_stream_index( + (stream_index, jobs_picked.clone()), + self.store.config.drainer_num_partitions, + ) + .await; + time::sleep(self.loop_interval).await; + } + + Ok(()) + } + + pub(crate) async fn shutdown_listener(&self, mut rx: mpsc::Receiver<()>) { + while let Some(_c) = rx.recv().await { + logger::info!("Awaiting shutdown!"); + metrics::SHUTDOWN_SIGNAL_RECEIVED.add(&metrics::CONTEXT, 1, &[]); + let shutdown_started = tokio::time::Instant::now(); + rx.close(); + + //Check until the active tasks are zero. This does not include the tasks in the stream. + while self.active_tasks.load(atomic::Ordering::SeqCst) != 0 { + time::sleep(self.shutdown_interval).await; + } + logger::info!("Terminating drainer"); + metrics::SUCCESSFUL_SHUTDOWN.add(&metrics::CONTEXT, 1, &[]); + let shutdown_ended = shutdown_started.elapsed().as_secs_f64() * 1000f64; + metrics::CLEANUP_TIME.record(&metrics::CONTEXT, shutdown_ended, &[]); + self.close(); + } + logger::info!( + tasks_remaining = self.active_tasks.load(atomic::Ordering::SeqCst), + "Drainer shutdown successfully" + ) + } + + pub fn spawn_error_handlers(&self, tx: mpsc::Sender<()>) -> errors::DrainerResult<()> { + let (redis_error_tx, redis_error_rx) = oneshot::channel(); + + let redis_conn_clone = self.store.redis_conn.clone(); + + // Spawn a task to monitor if redis is down or not + tokio::spawn(async move { redis_conn_clone.on_error(redis_error_tx).await }); + + //Spawns a task to send shutdown signal if redis goes down + tokio::spawn(redis_error_receiver(redis_error_rx, tx)); + + Ok(()) + } +} + +pub async fn redis_error_receiver(rx: oneshot::Receiver<()>, shutdown_channel: mpsc::Sender<()>) { + match rx.await { + Ok(_) => { + logger::error!("The redis server failed "); + let _ = shutdown_channel.send(()).await.map_err(|err| { + logger::error!("Failed to send signal to the shutdown channel {err}") + }); + } + Err(err) => { + logger::error!("Channel receiver error{err}"); + } + } +} + +#[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 = store.get_drainer_stream_name(stream_index); + + 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) + } + + let flag_stream_name = store.get_stream_key_flag(stream_index); + + let output = store.make_stream_available(flag_stream_name.as_str()).await; + active_tasks.fetch_sub(1, atomic::Ordering::Release); + output.map_err(|err| { + logger::error!(operation = "unlock_stream", err=?err); + err + }) +} + +#[instrument(skip_all, fields(global_id, request_id, session_id))] +async fn drainer( + store: Arc, + max_read_count: u64, + stream_name: &str, + jobs_picked: Arc, +) -> errors::DrainerResult<()> { + let stream_read = match store.read_from_stream(stream_name, max_read_count).await { + 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 = + redis_err.current_context() + { + metrics::STREAM_EMPTY.add(&metrics::CONTEXT, 1, &[]); + return Ok(()); + } else { + return Err(error); + } + } else { + return Err(error); + } + } + }; + + // parse_stream_entries returns error if no entries is found, handle it + let entries = utils::parse_stream_entries(&stream_read, stream_name)?; + let read_count = entries.len(); + + metrics::JOBS_PICKED_PER_STREAM.add( + &metrics::CONTEXT, + u64::try_from(read_count).unwrap_or(u64::MIN), + &[metrics::KeyValue { + key: "stream".into(), + value: stream_name.to_string().into(), + }], + ); + + let session_id = common_utils::generate_id_with_default_len("drainer_session"); + + let mut last_processed_id = String::new(); + + for (entry_id, entry) in entries.clone() { + let data = match StreamData::from_hashmap(entry) { + Ok(data) => data, + Err(err) => { + logger::error!(operation = "deserialization", err=?err); + metrics::STREAM_PARSE_FAIL.add( + &metrics::CONTEXT, + 1, + &[metrics::KeyValue { + key: "operation".into(), + value: "deserialization".into(), + }], + ); + + // break from the loop in case of a deser error + break; + } + }; + + tracing::Span::current().record("request_id", data.request_id); + tracing::Span::current().record("global_id", data.global_id); + tracing::Span::current().record("session_id", &session_id); + + match data.typed_sql.execute_query(&store, data.pushed_at).await { + Ok(_) => { + last_processed_id = entry_id; + } + Err(err) => match err.current_context() { + // In case of Uniqueviolation we can't really do anything to fix it so just clear + // it from the stream + diesel_models::errors::DatabaseError::UniqueViolation => { + last_processed_id = entry_id; + } + // break from the loop in case of an error in query + _ => break, + }, + } + } + + if !last_processed_id.is_empty() { + let entries_trimmed = store + .trim_from_stream(stream_name, &last_processed_id) + .await?; + if read_count != entries_trimmed { + logger::error!( + read_entries = %read_count, + trimmed_entries = %entries_trimmed, + ?entries, + "Assertion Failed no. of entries read from the stream doesn't match no. of entries trimmed" + ); + } + } else { + logger::error!(read_entries = %read_count,?entries,"No streams were processed in this session"); + } + + Ok(()) +} diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 7ccfd600d662..abb32c877962 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -1,34 +1,30 @@ mod connection; pub mod errors; +mod handler; pub mod logger; pub(crate) mod metrics; +mod query; pub mod services; pub mod settings; +mod stream; +mod types; mod utils; -use std::sync::{atomic, Arc}; +use std::sync::Arc; use common_utils::signals::get_allowed_signals; use diesel_models::kv; use error_stack::{IntoReport, ResultExt}; use router_env::{instrument, tracing}; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::mpsc; -use crate::{connection::pg_connection, services::Store}; +use crate::{ + connection::pg_connection, services::Store, settings::DrainerSettings, types::StreamData, +}; -pub async fn start_drainer( - store: Arc, - number_of_streams: u8, - max_read_count: u64, - shutdown_interval: u32, - loop_interval: u32, -) -> errors::DrainerResult<()> { - let mut stream_index: u8 = 0; - let mut jobs_picked: u8 = 0; +pub async fn start_drainer(store: Arc, conf: DrainerSettings) -> errors::DrainerResult<()> { + let drainer_handler = handler::Handler::from_conf(conf, store); - let mut shutdown_interval = - tokio::time::interval(std::time::Duration::from_millis(shutdown_interval.into())); - let mut loop_interval = - tokio::time::interval(std::time::Duration::from_millis(loop_interval.into())); + let (tx, rx) = mpsc::channel::<()>(1); let signal = get_allowed_signals() @@ -36,309 +32,20 @@ pub async fn start_drainer( .change_context(errors::DrainerError::SignalError( "Failed while getting allowed signals".to_string(), ))?; - - let (redis_error_tx, redis_error_rx) = oneshot::channel(); - let (tx, mut rx) = mpsc::channel(1); let handle = signal.handle(); let task_handle = tokio::spawn(common_utils::signals::signal_handler(signal, tx.clone())); - let redis_conn_clone = store.redis_conn.clone(); + let handler_clone = drainer_handler.clone(); - // Spawn a task to monitor if redis is down or not - tokio::spawn(async move { redis_conn_clone.on_error(redis_error_tx).await }); + tokio::task::spawn(async move { handler_clone.shutdown_listener(rx).await }); - //Spawns a task to send shutdown signal if redis goes down - tokio::spawn(redis_error_receiver(redis_error_rx, tx)); + drainer_handler.spawn_error_handlers(tx)?; + drainer_handler.spawn().await?; - let active_tasks = Arc::new(atomic::AtomicU64::new(0)); - 'event: loop { - metrics::DRAINER_HEALTH.add(&metrics::CONTEXT, 1, &[]); - match rx.try_recv() { - Err(mpsc::error::TryRecvError::Empty) => { - if utils::is_stream_available(stream_index, store.clone()).await { - tokio::spawn(drainer_handler( - store.clone(), - stream_index, - max_read_count, - active_tasks.clone(), - )); - jobs_picked += 1; - } - (stream_index, jobs_picked) = utils::increment_stream_index( - (stream_index, jobs_picked), - number_of_streams, - &mut loop_interval, - ) - .await; - } - Ok(()) | Err(mpsc::error::TryRecvError::Disconnected) => { - logger::info!("Awaiting shutdown!"); - metrics::SHUTDOWN_SIGNAL_RECEIVED.add(&metrics::CONTEXT, 1, &[]); - let shutdown_started = tokio::time::Instant::now(); - rx.close(); - loop { - if active_tasks.load(atomic::Ordering::Acquire) == 0 { - logger::info!("Terminating drainer"); - metrics::SUCCESSFUL_SHUTDOWN.add(&metrics::CONTEXT, 1, &[]); - let shutdown_ended = shutdown_started.elapsed().as_secs_f64() * 1000f64; - metrics::CLEANUP_TIME.record(&metrics::CONTEXT, shutdown_ended, &[]); - break 'event; - } - shutdown_interval.tick().await; - } - } - } - } handle.close(); - task_handle + let _ = task_handle .await - .into_report() - .change_context(errors::DrainerError::UnexpectedError( - "Failed while joining signal handler".to_string(), - ))?; + .map_err(|err| logger::error!("Failed while joining signal handler: {:?}", err)); Ok(()) } - -pub async fn redis_error_receiver(rx: oneshot::Receiver<()>, shutdown_channel: mpsc::Sender<()>) { - match rx.await { - Ok(_) => { - logger::error!("The redis server failed "); - let _ = shutdown_channel.send(()).await.map_err(|err| { - logger::error!("Failed to send signal to the shutdown channel {err}") - }); - } - Err(err) => { - logger::error!("Channel receiver error{err}"); - } - } -} - -async fn drainer_handler( - store: Arc, - stream_index: u8, - max_read_count: u64, - active_tasks: 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; - - if let Err(error) = drainer_result { - logger::error!(?error) - } - - let flag_stream_name = utils::get_stream_key_flag(store.clone(), stream_index); - - //TODO: USE THE RESULT FOR LOGGING - let output = - utils::make_stream_available(flag_stream_name.as_str(), store.redis_conn.as_ref()).await; - active_tasks.fetch_sub(1, atomic::Ordering::Release); - output -} - -#[instrument(skip_all, fields(global_id, request_id, session_id))] -async fn drainer( - store: Arc, - max_read_count: u64, - stream_name: &str, -) -> errors::DrainerResult<()> { - let stream_read = - match utils::read_from_stream(stream_name, max_read_count, store.redis_conn.as_ref()).await - { - Ok(result) => result, - Err(error) => { - if let errors::DrainerError::RedisError(redis_err) = error.current_context() { - if let redis_interface::errors::RedisError::StreamEmptyOrNotAvailable = - redis_err.current_context() - { - metrics::STREAM_EMPTY.add(&metrics::CONTEXT, 1, &[]); - return Ok(()); - } else { - return Err(error); - } - } else { - return Err(error); - } - } - }; - // parse_stream_entries returns error if no entries is found, handle it - let (entries, last_entry_id) = utils::parse_stream_entries(&stream_read, stream_name)?; - let read_count = entries.len(); - - metrics::JOBS_PICKED_PER_STREAM.add( - &metrics::CONTEXT, - u64::try_from(read_count).unwrap_or(u64::MIN), - &[metrics::KeyValue { - key: "stream".into(), - value: stream_name.to_string().into(), - }], - ); - - let session_id = common_utils::generate_id_with_default_len("drainer_session"); - - // TODO: Handle errors when deserialization fails and when DB error occurs - for entry in entries { - let typed_sql = entry.1.get("typed_sql").map_or(String::new(), Clone::clone); - let request_id = entry - .1 - .get("request_id") - .map_or(String::new(), Clone::clone); - let global_id = entry.1.get("global_id").map_or(String::new(), Clone::clone); - - tracing::Span::current().record("request_id", request_id); - tracing::Span::current().record("global_id", global_id); - tracing::Span::current().record("session_id", &session_id); - - let result = serde_json::from_str::(&typed_sql); - let db_op = match result { - Ok(f) => f, - Err(_err) => continue, // TODO: handle error - }; - - let conn = pg_connection(&store.master_pool).await; - let insert_op = "insert"; - let update_op = "update"; - let payment_intent = "payment_intent"; - let payment_attempt = "payment_attempt"; - let refund = "refund"; - let reverse_lookup = "reverse_lookup"; - let address = "address"; - match db_op { - // TODO: Handle errors - kv::DBOperation::Insert { insertable } => { - let (_, execution_time) = common_utils::date_time::time_it(|| async { - match insertable { - kv::Insertable::PaymentIntent(a) => { - macro_util::handle_resp!( - a.insert(&conn).await, - insert_op, - payment_intent - ) - } - kv::Insertable::PaymentAttempt(a) => { - macro_util::handle_resp!( - a.insert(&conn).await, - insert_op, - payment_attempt - ) - } - kv::Insertable::Refund(a) => { - macro_util::handle_resp!(a.insert(&conn).await, insert_op, refund) - } - kv::Insertable::Address(addr) => { - macro_util::handle_resp!(addr.insert(&conn).await, insert_op, address) - } - kv::Insertable::ReverseLookUp(rev) => { - macro_util::handle_resp!( - rev.insert(&conn).await, - insert_op, - reverse_lookup - ) - } - } - }) - .await; - metrics::QUERY_EXECUTION_TIME.record( - &metrics::CONTEXT, - execution_time, - &[metrics::KeyValue { - key: "operation".into(), - value: insert_op.into(), - }], - ); - } - kv::DBOperation::Update { updatable } => { - let (_, execution_time) = common_utils::date_time::time_it(|| async { - match updatable { - kv::Updateable::PaymentIntentUpdate(a) => { - macro_util::handle_resp!( - a.orig.update(&conn, a.update_data).await, - update_op, - payment_intent - ) - } - kv::Updateable::PaymentAttemptUpdate(a) => { - macro_util::handle_resp!( - a.orig.update_with_attempt_id(&conn, a.update_data).await, - update_op, - payment_attempt - ) - } - kv::Updateable::RefundUpdate(a) => { - macro_util::handle_resp!( - a.orig.update(&conn, a.update_data).await, - update_op, - refund - ) - } - kv::Updateable::AddressUpdate(a) => macro_util::handle_resp!( - a.orig.update(&conn, a.update_data).await, - update_op, - address - ), - } - }) - .await; - metrics::QUERY_EXECUTION_TIME.record( - &metrics::CONTEXT, - execution_time, - &[metrics::KeyValue { - key: "operation".into(), - value: update_op.into(), - }], - ); - } - kv::DBOperation::Delete => { - // [#224]: Implement this - logger::error!("Not implemented!"); - } - }; - } - - let entries_trimmed = - utils::trim_from_stream(stream_name, last_entry_id.as_str(), &store.redis_conn).await?; - - if read_count != entries_trimmed { - logger::error!( - read_entries = %read_count, - trimmed_entries = %entries_trimmed, - ?entries, - "Assertion Failed no. of entries read from the stream doesn't match no. of entries trimmed" - ); - } - - Ok(()) -} - -mod macro_util { - - macro_rules! handle_resp { - ($result:expr,$op_type:expr, $table:expr) => { - match $result { - Ok(inner_result) => { - logger::info!(operation = %$op_type, table = %$table, ?inner_result); - metrics::SUCCESSFUL_QUERY_EXECUTION.add(&metrics::CONTEXT, 1, &[ - metrics::KeyValue { - key: "operation".into(), - value: $table.into(), - } - ]); - } - Err(err) => { - logger::error!(operation = %$op_type, table = %$table, ?err); - metrics::ERRORS_WHILE_QUERY_EXECUTION.add(&metrics::CONTEXT, 1, &[ - metrics::KeyValue { - key: "operation".into(), - value: $table.into(), - } - ]); - } - } - }; - } - pub(crate) use handle_resp; -} diff --git a/crates/drainer/src/main.rs b/crates/drainer/src/main.rs index 2b4142abc0c7..34c1294d55fa 100644 --- a/crates/drainer/src/main.rs +++ b/crates/drainer/src/main.rs @@ -15,10 +15,8 @@ async fn main() -> DrainerResult<()> { let store = services::Store::new(&conf, false).await; let store = std::sync::Arc::new(store); - let number_of_streams = store.config.drainer_num_partitions; - let max_read_count = conf.drainer.max_read_count; - let shutdown_intervals = conf.drainer.shutdown_interval; - let loop_interval = conf.drainer.loop_interval; + #[cfg(feature = "vergen")] + println!("Starting drainer (Version: {})", router_env::git_tag!()); let _guard = router_env::setup( &conf.log, @@ -29,14 +27,7 @@ async fn main() -> DrainerResult<()> { logger::debug!(startup_config=?conf); logger::info!("Drainer started [{:?}] [{:?}]", conf.drainer, conf.log); - start_drainer( - store.clone(), - number_of_streams, - max_read_count, - shutdown_intervals, - loop_interval, - ) - .await?; + start_drainer(store.clone(), conf.drainer).await?; Ok(()) } diff --git a/crates/drainer/src/metrics.rs b/crates/drainer/src/metrics.rs index 77f3d5e7db1d..750f23bc73b5 100644 --- a/crates/drainer/src/metrics.rs +++ b/crates/drainer/src/metrics.rs @@ -1,5 +1,7 @@ pub use router_env::opentelemetry::KeyValue; -use router_env::{counter_metric, global_meter, histogram_metric, metrics_context}; +use router_env::{ + counter_metric, global_meter, histogram_metric, histogram_metric_i64, metrics_context, +}; metrics_context!(CONTEXT); global_meter!(DRAINER_METER, "DRAINER"); @@ -12,9 +14,11 @@ counter_metric!(SUCCESSFUL_QUERY_EXECUTION, DRAINER_METER); counter_metric!(SHUTDOWN_SIGNAL_RECEIVED, DRAINER_METER); counter_metric!(SUCCESSFUL_SHUTDOWN, DRAINER_METER); counter_metric!(STREAM_EMPTY, DRAINER_METER); +counter_metric!(STREAM_PARSE_FAIL, DRAINER_METER); counter_metric!(DRAINER_HEALTH, DRAINER_METER); histogram_metric!(QUERY_EXECUTION_TIME, DRAINER_METER); // Time in (ms) milliseconds histogram_metric!(REDIS_STREAM_READ_TIME, DRAINER_METER); // Time in (ms) milliseconds histogram_metric!(REDIS_STREAM_TRIM_TIME, DRAINER_METER); // Time in (ms) milliseconds histogram_metric!(CLEANUP_TIME, DRAINER_METER); // Time in (ms) milliseconds +histogram_metric_i64!(DRAINER_DELAY_SECONDS, DRAINER_METER); // Time in (s) seconds diff --git a/crates/drainer/src/query.rs b/crates/drainer/src/query.rs new file mode 100644 index 000000000000..a1e04fb6d0f1 --- /dev/null +++ b/crates/drainer/src/query.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use common_utils::errors::CustomResult; +use diesel_models::errors::DatabaseError; + +use crate::{kv, logger, metrics, pg_connection, services::Store}; + +#[async_trait::async_trait] +pub trait ExecuteQuery { + async fn execute_query( + self, + store: &Arc, + pushed_at: i64, + ) -> CustomResult<(), DatabaseError>; +} + +#[async_trait::async_trait] +impl ExecuteQuery for kv::DBOperation { + async fn execute_query( + self, + store: &Arc, + pushed_at: i64, + ) -> CustomResult<(), DatabaseError> { + let conn = pg_connection(&store.master_pool).await; + let operation = self.operation(); + let table = self.table(); + + let tags: &[metrics::KeyValue] = &[ + metrics::KeyValue { + key: "operation".into(), + value: operation.into(), + }, + metrics::KeyValue { + key: "table".into(), + value: table.into(), + }, + ]; + + let (result, execution_time) = + Box::pin(common_utils::date_time::time_it(|| self.execute(&conn))).await; + + push_drainer_delay(pushed_at, operation, table, tags); + metrics::QUERY_EXECUTION_TIME.record(&metrics::CONTEXT, execution_time, tags); + + match result { + Ok(result) => { + logger::info!(operation = operation, table = table, ?result); + metrics::SUCCESSFUL_QUERY_EXECUTION.add(&metrics::CONTEXT, 1, tags); + Ok(()) + } + Err(err) => { + logger::error!(operation = operation, table = table, ?err); + metrics::ERRORS_WHILE_QUERY_EXECUTION.add(&metrics::CONTEXT, 1, tags); + Err(err) + } + } + } +} + +#[inline(always)] +fn push_drainer_delay(pushed_at: i64, operation: &str, table: &str, tags: &[metrics::KeyValue]) { + let drained_at = common_utils::date_time::now_unix_timestamp(); + let delay = drained_at - pushed_at; + + logger::debug!( + operation = operation, + table = table, + delay = format!("{delay} secs") + ); + + metrics::DRAINER_DELAY_SECONDS.record(&metrics::CONTEXT, delay, tags); +} diff --git a/crates/drainer/src/services.rs b/crates/drainer/src/services.rs index 73f66f27dbf5..4393ebb9dc97 100644 --- a/crates/drainer/src/services.rs +++ b/crates/drainer/src/services.rs @@ -17,6 +17,11 @@ pub struct StoreConfig { } impl Store { + /// # Panics + /// + /// Panics if there is a failure while obtaining the HashiCorp client using the provided configuration. + /// This panic indicates a critical failure in setting up external services, and the application cannot proceed without a valid HashiCorp client. + /// pub async fn new(config: &crate::settings::Settings, test_transaction: bool) -> Self { Self { master_pool: diesel_make_pg_pool( @@ -24,6 +29,11 @@ impl Store { test_transaction, #[cfg(feature = "kms")] external_services::kms::get_kms_client(&config.kms).await, + #[cfg(feature = "hashicorp-vault")] + #[allow(clippy::expect_used)] + external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + .await + .expect("Failed while getting hashicorp client"), ) .await, redis_conn: Arc::new(crate::connection::redis_connection(config).await), @@ -34,9 +44,4 @@ impl Store { request_id: None, } } - - pub fn drainer_stream(&self, shard_key: &str) -> String { - // Example: {shard_5}_drainer_stream - format!("{{{}}}_{}", shard_key, self.config.drainer_stream_name,) - } } diff --git a/crates/drainer/src/settings.rs b/crates/drainer/src/settings.rs index cc64a99e463c..5b80ee375f54 100644 --- a/crates/drainer/src/settings.rs +++ b/crates/drainer/src/settings.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use common_utils::ext_traits::ConfigExt; use config::{Environment, File}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; #[cfg(feature = "kms")] use external_services::kms; use redis_interface as redis; @@ -34,6 +36,8 @@ pub struct Settings { pub drainer: DrainerSettings, #[cfg(feature = "kms")] pub kms: kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + pub hc_vault: hashicorp_vault::HashiCorpVaultConfig, } #[derive(Debug, Deserialize, Clone)] @@ -79,7 +83,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/stream.rs b/crates/drainer/src/stream.rs new file mode 100644 index 000000000000..b2775ac4ba1d --- /dev/null +++ b/crates/drainer/src/stream.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; + +use error_stack::IntoReport; +use redis_interface as redis; +use router_env::{logger, tracing}; + +use crate::{errors, metrics, Store}; + +pub type StreamEntries = Vec<(String, HashMap)>; +pub type StreamReadResult = HashMap; + +impl Store { + #[inline(always)] + pub fn drainer_stream(&self, shard_key: &str) -> String { + // Example: {shard_5}_drainer_stream + format!("{{{}}}_{}", shard_key, self.config.drainer_stream_name,) + } + + #[inline(always)] + pub(crate) fn get_stream_key_flag(&self, stream_index: u8) -> String { + format!("{}_in_use", self.get_drainer_stream_name(stream_index)) + } + + #[inline(always)] + pub(crate) fn get_drainer_stream_name(&self, stream_index: u8) -> String { + self.drainer_stream(format!("shard_{stream_index}").as_str()) + } + + #[router_env::instrument(skip_all)] + pub async fn is_stream_available(&self, stream_index: u8) -> bool { + let stream_key_flag = self.get_stream_key_flag(stream_index); + + match self + .redis_conn + .set_key_if_not_exists_with_expiry(stream_key_flag.as_str(), true, None) + .await + { + Ok(resp) => resp == redis::types::SetnxReply::KeySet, + Err(error) => { + logger::error!(operation="lock_stream",err=?error); + false + } + } + } + + pub async fn make_stream_available(&self, stream_name_flag: &str) -> errors::DrainerResult<()> { + match self.redis_conn.delete_key(stream_name_flag).await { + Ok(redis::DelReply::KeyDeleted) => Ok(()), + Ok(redis::DelReply::KeyNotDeleted) => { + logger::error!("Tried to unlock a stream which is already unlocked"); + Ok(()) + } + Err(error) => Err(errors::DrainerError::from(error).into()), + } + } + + pub async fn read_from_stream( + &self, + stream_name: &str, + max_read_count: u64, + ) -> errors::DrainerResult { + // "0-0" id gives first entry + let stream_id = "0-0"; + let (output, execution_time) = common_utils::date_time::time_it(|| async { + self.redis_conn + .stream_read_entries(stream_name, stream_id, Some(max_read_count)) + .await + .map_err(errors::DrainerError::from) + .into_report() + }) + .await; + + metrics::REDIS_STREAM_READ_TIME.record( + &metrics::CONTEXT, + execution_time, + &[metrics::KeyValue::new("stream", stream_name.to_owned())], + ); + + output + } + pub async fn trim_from_stream( + &self, + stream_name: &str, + minimum_entry_id: &str, + ) -> errors::DrainerResult { + let trim_kind = redis::StreamCapKind::MinID; + let trim_type = redis::StreamCapTrim::Exact; + let trim_id = minimum_entry_id; + let (trim_result, execution_time) = + common_utils::date_time::time_it::, _, _>(|| async { + let trim_result = self + .redis_conn + .stream_trim_entries(stream_name, (trim_kind, trim_type, trim_id)) + .await + .map_err(errors::DrainerError::from) + .into_report()?; + + // Since xtrim deletes entries below given id excluding the given id. + // Hence, deleting the minimum entry id + self.redis_conn + .stream_delete_entries(stream_name, minimum_entry_id) + .await + .map_err(errors::DrainerError::from) + .into_report()?; + + Ok(trim_result) + }) + .await; + + metrics::REDIS_STREAM_TRIM_TIME.record( + &metrics::CONTEXT, + execution_time, + &[metrics::KeyValue::new("stream", stream_name.to_owned())], + ); + + // adding 1 because we are deleting the given id too + Ok(trim_result? + 1) + } +} diff --git a/crates/drainer/src/types.rs b/crates/drainer/src/types.rs new file mode 100644 index 000000000000..f1ddf8ef27e9 --- /dev/null +++ b/crates/drainer/src/types.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use common_utils::errors; +use error_stack::{IntoReport, ResultExt}; +use serde::{de::value::MapDeserializer, Deserialize, Serialize}; + +use crate::{ + kv, + utils::{deserialize_db_op, deserialize_i64}, +}; + +#[derive(Deserialize, Serialize)] +pub struct StreamData { + pub request_id: String, + pub global_id: String, + #[serde(deserialize_with = "deserialize_db_op")] + pub typed_sql: kv::DBOperation, + #[serde(deserialize_with = "deserialize_i64")] + pub pushed_at: i64, +} + +impl StreamData { + pub fn from_hashmap( + hashmap: HashMap, + ) -> errors::CustomResult { + let iter = MapDeserializer::< + '_, + std::collections::hash_map::IntoIter, + serde_json::error::Error, + >::new(hashmap.into_iter()); + + Self::deserialize(iter) + .into_report() + .change_context(errors::ParsingError::StructParseFailure("StreamData")) + } +} diff --git a/crates/drainer/src/utils.rs b/crates/drainer/src/utils.rs index 5a995652bb11..e27e04c30e68 100644 --- a/crates/drainer/src/utils.rs +++ b/crates/drainer/src/utils.rs @@ -1,121 +1,20 @@ -use std::{collections::HashMap, sync::Arc}; +use std::sync::{atomic, Arc}; use error_stack::IntoReport; use redis_interface as redis; +use serde::de::Deserialize; use crate::{ - errors::{self, DrainerError}, - logger, metrics, services, + errors, kv, metrics, + stream::{StreamEntries, StreamReadResult}, }; -pub type StreamEntries = Vec<(String, HashMap)>; -pub type StreamReadResult = HashMap; - -pub async fn is_stream_available(stream_index: u8, store: Arc) -> bool { - let stream_key_flag = get_stream_key_flag(store.clone(), stream_index); - - match store - .redis_conn - .set_key_if_not_exists_with_expiry(stream_key_flag.as_str(), true, None) - .await - { - Ok(resp) => resp == redis::types::SetnxReply::KeySet, - Err(error) => { - logger::error!(?error); - // Add metrics or logs - false - } - } -} - -pub async fn read_from_stream( - stream_name: &str, - max_read_count: u64, - redis: &redis::RedisConnectionPool, -) -> errors::DrainerResult { - // "0-0" id gives first entry - let stream_id = "0-0"; - let (output, execution_time) = common_utils::date_time::time_it(|| async { - redis - .stream_read_entries(stream_name, stream_id, Some(max_read_count)) - .await - .map_err(DrainerError::from) - .into_report() - }) - .await; - - metrics::REDIS_STREAM_READ_TIME.record( - &metrics::CONTEXT, - execution_time, - &[metrics::KeyValue::new("stream", stream_name.to_owned())], - ); - - output -} - -pub async fn trim_from_stream( - stream_name: &str, - minimum_entry_id: &str, - redis: &redis::RedisConnectionPool, -) -> errors::DrainerResult { - let trim_kind = redis::StreamCapKind::MinID; - let trim_type = redis::StreamCapTrim::Exact; - let trim_id = minimum_entry_id; - let (trim_result, execution_time) = - common_utils::date_time::time_it::, _, _>(|| async { - let trim_result = redis - .stream_trim_entries(stream_name, (trim_kind, trim_type, trim_id)) - .await - .map_err(DrainerError::from) - .into_report()?; - - // Since xtrim deletes entries below given id excluding the given id. - // Hence, deleting the minimum entry id - redis - .stream_delete_entries(stream_name, minimum_entry_id) - .await - .map_err(DrainerError::from) - .into_report()?; - - Ok(trim_result) - }) - .await; - - metrics::REDIS_STREAM_TRIM_TIME.record( - &metrics::CONTEXT, - execution_time, - &[metrics::KeyValue::new("stream", stream_name.to_owned())], - ); - - // adding 1 because we are deleting the given id too - Ok(trim_result? + 1) -} - -pub async fn make_stream_available( - stream_name_flag: &str, - redis: &redis::RedisConnectionPool, -) -> errors::DrainerResult<()> { - match redis.delete_key(stream_name_flag).await { - Ok(redis::DelReply::KeyDeleted) => Ok(()), - Ok(redis::DelReply::KeyNotDeleted) => { - logger::error!("Tried to unlock a stream which is already unlocked"); - Ok(()) - } - Err(error) => Err(DrainerError::from(error).into()), - } -} - pub fn parse_stream_entries<'a>( read_result: &'a StreamReadResult, stream_name: &str, -) -> errors::DrainerResult<(&'a StreamEntries, String)> { +) -> errors::DrainerResult<&'a StreamEntries> { read_result .get(stream_name) - .and_then(|entries| { - entries - .last() - .map(|last_entry| (entries, last_entry.0.clone())) - }) .ok_or_else(|| { errors::DrainerError::RedisError(error_stack::report!( redis::errors::RedisError::NotFound @@ -124,29 +23,55 @@ pub fn parse_stream_entries<'a>( .into_report() } +pub(crate) fn deserialize_i64<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s = serde_json::Value::deserialize(deserializer)?; + match s { + serde_json::Value::String(str_val) => str_val.parse().map_err(serde::de::Error::custom), + serde_json::Value::Number(num_val) => match num_val.as_i64() { + Some(val) => Ok(val), + None => Err(serde::de::Error::custom(format!( + "could not convert {num_val:?} to i64" + ))), + }, + other => Err(serde::de::Error::custom(format!( + "unexpected data format - expected string or number, got: {other:?}" + ))), + } +} + +pub(crate) fn deserialize_db_op<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s = serde_json::Value::deserialize(deserializer)?; + match s { + serde_json::Value::String(str_val) => { + serde_json::from_str(&str_val).map_err(serde::de::Error::custom) + } + other => Err(serde::de::Error::custom(format!( + "unexpected data format - expected string got: {other:?}" + ))), + } +} + // Here the output is in the format (stream_index, jobs_picked), // similar to the first argument of the function +#[inline(always)] 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 } } - -pub(crate) fn get_stream_key_flag(store: Arc, stream_index: u8) -> String { - format!("{}_in_use", get_drainer_stream_name(store, stream_index)) -} - -pub(crate) fn get_drainer_stream_name(store: Arc, stream_index: u8) -> String { - store.drainer_stream(format!("shard_{stream_index}").as_str()) -} diff --git a/crates/euclid/Cargo.toml b/crates/euclid/Cargo.toml index 859795964145..415398105aef 100644 --- a/crates/euclid/Cargo.toml +++ b/crates/euclid/Cargo.toml @@ -12,10 +12,11 @@ frunk_core = "0.4.1" nom = { version = "7.1.3", features = ["alloc"], optional = true } once_cell = "1.18.0" rustc-hash = "1.1.0" -serde = { version = "1.0.163", features = ["derive", "rc"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive", "rc"] } +serde_json = "1.0.108" strum = { version = "0.25", features = ["derive"] } thiserror = "1.0.43" +utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party dependencies common_enums = { version = "0.1.0", path = "../common_enums" } 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/dssa/state_machine.rs b/crates/euclid/src/dssa/state_machine.rs index 4cd53911dfe4..93d394eece97 100644 --- a/crates/euclid/src/dssa/state_machine.rs +++ b/crates/euclid/src/dssa/state_machine.rs @@ -678,7 +678,9 @@ mod tests { .collect::>(); assert_eq!( values, - expected_contexts[expected_idx] + expected_contexts + .get(expected_idx) + .expect("Error deriving contexts") .iter() .collect::>() ); @@ -702,7 +704,9 @@ mod tests { .collect::>(); assert_eq!( values, - expected_contexts[expected_idx] + expected_contexts + .get(expected_idx) + .expect("Error deriving contexts") .iter() .collect::>() ); diff --git a/crates/euclid/src/enums.rs b/crates/euclid/src/enums.rs index dc6d9f66a58f..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,105 +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, - Bambora, - Bankofamerica, - Bitpay, - 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, - Prophetpay, - 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..905bf04de0af 100644 --- a/crates/euclid/src/frontend/ast.rs +++ b/crates/euclid/src/frontend/ast.rs @@ -2,28 +2,27 @@ pub mod lowering; #[cfg(feature = "ast_parser")] pub mod parser; +use common_enums::RoutableConnectors; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -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, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] pub struct MetadataValue { pub key: String, pub value: String, } /// Represents a value in the DSL -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] #[serde(tag = "type", content = "value", rename_all = "snake_case")] pub enum ValueType { /// Represents a number literal @@ -62,7 +61,7 @@ impl ValueType { } /// Represents a number comparison for "NumberComparisonArrayValue" -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct NumberComparison { pub comparison_type: ComparisonType, @@ -70,7 +69,7 @@ pub struct NumberComparison { } /// Conditional comparison type -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum ComparisonType { Equal, @@ -82,7 +81,7 @@ pub enum ComparisonType { } /// Represents a single comparison condition. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct Comparison { /// The left hand side which will always be a domain input identifier like "payment.method.cardtype" @@ -94,6 +93,7 @@ pub struct Comparison { /// Additional metadata that the Static Analyzer and Backend does not touch. /// This can be used to store useful information for the frontend and is required for communication /// between the static analyzer and the frontend. + #[schema(value_type=HashMap)] pub metadata: Metadata, } @@ -114,9 +114,10 @@ pub type IfCondition = Vec; /// } /// } /// ``` -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct IfStatement { + #[schema(value_type=Vec)] pub condition: IfCondition, pub nested: Option>, } @@ -136,8 +137,9 @@ pub struct IfStatement { /// } /// ``` -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] +#[aliases(RuleConnectorSelection = Rule)] pub struct Rule { pub name: String, #[serde(alias = "routingOutput")] @@ -147,10 +149,43 @@ pub struct Rule { /// The program, having a default connector selection and /// a bunch of rules. Also can hold arbitrary metadata. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] +#[aliases(ProgramConnectorSelection = Program)] pub struct Program { pub default_selection: O, + #[schema(value_type=RuleConnectorSelection)] pub rules: Vec>, + #[schema(value_type=HashMap)] pub metadata: Metadata, } + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct RoutableConnectorChoice { + #[cfg(feature = "connector_choice_bcompat")] + #[serde(skip)] + pub choice_kind: RoutableChoiceKind, + #[cfg(feature = "connector_choice_mca_id")] + pub merchant_connector_id: Option, + #[cfg(not(feature = "connector_choice_mca_id"))] + pub sub_label: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub enum RoutableChoiceKind { + OnlyConnector, + FullStruct, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ConnectorVolumeSplit { + pub connector: RoutableConnectorChoice, + pub split: u8, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum ConnectorSelection { + Priority(Vec), + VolumeSplit(Vec), +} diff --git a/crates/euclid/src/frontend/dir.rs b/crates/euclid/src/frontend/dir.rs index 7f2fc252d232..dc81359c51d5 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, @@ -722,7 +722,10 @@ mod test { }; let display_str = key.to_string(); - assert_eq!(&json_str[1..json_str.len() - 1], display_str); + assert_eq!( + json_str.get(1..json_str.len() - 1).expect("Value metadata"), + display_str + ); key_names.insert(key, display_str); } diff --git a/crates/euclid/src/frontend/dir/enums.rs b/crates/euclid/src/frontend/dir/enums.rs index f049ad35328e..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( diff --git a/crates/euclid_macros/src/inner/knowledge.rs b/crates/euclid_macros/src/inner/knowledge.rs index 73b94919c903..9f33a6871c56 100644 --- a/crates/euclid_macros/src/inner/knowledge.rs +++ b/crates/euclid_macros/src/inner/knowledge.rs @@ -417,7 +417,10 @@ impl GenContext { .position(|v| *v == node_id) .ok_or_else(|| "Error deciding cycle order".to_string())?; - let cycle_order = order[position..].to_vec(); + let cycle_order = order + .get(position..) + .ok_or_else(|| "Error getting cycle order".to_string())? + .to_vec(); Ok(Some(cycle_order)) } else if visited.contains(&node_id) { Ok(None) diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 4fc8cd970f40..13296bcde626 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -10,15 +10,23 @@ rust-version.workspace = true crate-type = ["cdylib"] [features] -default = ["connector_choice_bcompat"] +default = ["connector_choice_bcompat","payouts", "connector_choice_mca_id"] +release = ["connector_choice_bcompat", "connector_choice_mca_id"] connector_choice_bcompat = ["api_models/connector_choice_bcompat"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] -dummy_connector = ["kgraph_utils/dummy_connector"] +dummy_connector = ["kgraph_utils/dummy_connector", "connector_configs/dummy_connector"] +production = ["connector_configs/production"] +development = ["connector_configs/development"] +sandbox = ["connector_configs/sandbox"] +payouts = [] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +currency_conversion = { version = "0.1.0", path = "../currency_conversion" } +connector_configs = { version = "0.1.0", path = "../connector_configs" } 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..1143ea8a2817 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -6,7 +6,18 @@ use std::{ str::FromStr, }; -use api_models::{admin as admin_api, routing::ConnectorSelection}; +use api_models::{ + admin as admin_api, conditional_configs::ConditionalConfigs, enums as api_model_enums, + routing::ConnectorSelection, surcharge_decision_configs::SurchargeDecisionConfigs, +}; +use common_enums::RoutableConnectors; +use connector_configs::{ + common_config::{ConnectorApiIntegrationPayload, DashboardRequestPayload}, + connector, +}; +use currency_conversion::{ + conversion::convert as convert_currency, types as currency_conversion_types, +}; use euclid::{ backend::{inputs, interpreter::InterpreterBackend, EuclidBackend}, dssa::{ @@ -14,10 +25,9 @@ use euclid::{ graph::{self, Memoization}, state_machine, truth, }, - enums, frontend::{ ast, - dir::{self, enums as dir_enums}, + dir::{self, enums as dir_enums, EuclidDirFilter}, }, }; use once_cell::sync::OnceCell; @@ -33,6 +43,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 +87,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 +190,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)] @@ -167,6 +212,18 @@ pub fn get_key_type(key: &str) -> Result { Ok(key_str) } +#[wasm_bindgen(js_name = getThreeDsKeys)] +pub fn get_three_ds_keys() -> JsResult { + let keys = ::ALLOWED; + Ok(serde_wasm_bindgen::to_value(keys)?) +} + +#[wasm_bindgen(js_name= getSurchargeKeys)] +pub fn get_surcharge_keys() -> JsResult { + let keys = ::ALLOWED; + Ok(serde_wasm_bindgen::to_value(keys)?) +} + #[wasm_bindgen(js_name=parseToString)] pub fn parser(val: String) -> String { ron_parser::my_parse(val) @@ -216,12 +273,57 @@ pub fn add_two(n1: i64, n2: i64) -> i64 { } #[wasm_bindgen(js_name = getDescriptionCategory)] -pub fn get_description_category(key: &str) -> JsResult { - let key = dir::DirKeyKind::from_str(key).map_err(|_| "Invalid key received".to_string())?; +pub fn get_description_category() -> JsResult { + let keys = dir::DirKeyKind::VARIANTS + .iter() + .copied() + .filter(|s| s != &"Connector") + .collect::>(); + let mut category: HashMap, Vec>> = HashMap::new(); + for key in keys { + let dir_key = + dir::DirKeyKind::from_str(key).map_err(|_| "Invalid key received".to_string())?; + let details = types::Details { + description: dir_key.get_detailed_message(), + kind: dir_key.clone(), + }; + category + .entry(dir_key.get_str("Category")) + .and_modify(|val| val.push(details.clone())) + .or_insert(vec![details]); + } - let result = types::Details { - description: key.get_detailed_message(), - category: key.get_str("Category"), - }; + Ok(serde_wasm_bindgen::to_value(&category)?) +} + +#[wasm_bindgen(js_name = getConnectorConfig)] +pub fn get_connector_config(key: &str) -> JsResult { + let key = api_model_enums::Connector::from_str(key) + .map_err(|_| "Invalid key received".to_string())?; + let res = connector::ConnectorConfig::get_connector_config(key)?; + Ok(serde_wasm_bindgen::to_value(&res)?) +} + +#[cfg(feature = "payouts")] +#[wasm_bindgen(js_name = getPayoutConnectorConfig)] +pub fn get_payout_connector_config(key: &str) -> JsResult { + let key = api_model_enums::PayoutConnectors::from_str(key) + .map_err(|_| "Invalid key received".to_string())?; + let res = connector::ConnectorConfig::get_payout_connector_config(key)?; + Ok(serde_wasm_bindgen::to_value(&res)?) +} + +#[wasm_bindgen(js_name = getRequestPayload)] +pub fn get_request_payload(input: JsValue, response: JsValue) -> JsResult { + let input: DashboardRequestPayload = serde_wasm_bindgen::from_value(input)?; + let api_response: ConnectorApiIntegrationPayload = serde_wasm_bindgen::from_value(response)?; + let result = DashboardRequestPayload::create_connector_request(input, api_response); + Ok(serde_wasm_bindgen::to_value(&result)?) +} + +#[wasm_bindgen(js_name = getResponsePayload)] +pub fn get_response_payload(input: JsValue) -> JsResult { + let input: ConnectorApiIntegrationPayload = serde_wasm_bindgen::from_value(input)?; + let result = ConnectorApiIntegrationPayload::get_transformed_response_payload(input); Ok(serde_wasm_bindgen::to_value(&result)?) } diff --git a/crates/euclid_wasm/src/types.rs b/crates/euclid_wasm/src/types.rs index ea40449971bc..6353d9009c36 100644 --- a/crates/euclid_wasm/src/types.rs +++ b/crates/euclid_wasm/src/types.rs @@ -1,7 +1,8 @@ +use euclid::frontend::dir::DirKeyKind; use serde::Serialize; #[derive(Serialize, Clone)] pub struct Details<'a> { pub description: Option<&'a str>, - pub category: Option<&'a str>, + pub kind: DirKeyKind, } diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 4700c2a81d75..bf836af71a79 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -10,20 +10,28 @@ license.workspace = true [features] kms = ["dep:aws-config", "dep:aws-sdk-kms"] email = ["dep:aws-config"] +aws_s3 = ["dep:aws-config", "dep:aws-sdk-s3"] +hashicorp-vault = [ "dep:vaultrs" ] [dependencies] 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-sdk-s3 = { version = "0.28.0", optional = true } aws-smithy-client = "0.55.3" base64 = "0.21.2" dyn-clone = "1.0.11" error-stack = "0.3.1" once_cell = "1.18.0" -serde = { version = "1.0.163", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.40" -tokio = "1.28.2" +tokio = "1.35.1" +hyper-proxy = "0.9.1" +hyper = "0.14.26" +vaultrs = { version = "0.7.0", optional = true } +hex = "0.4.3" # 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..de4d77949185 --- /dev/null +++ b/crates/external_services/src/email/ses.rs @@ -0,0 +1,255 @@ +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 crate::email::{EmailClient, EmailError, EmailResult, EmailSettings, IntermediateString}; + +/// Client for AWS SES operation +#[derive(Debug, Clone)] +pub struct AwsSes { + 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 { + // Build the client initially which will help us know if the email configuration is correct + Self::create_client(conf, proxy_url) + .await + .map_err(|error| logger::error!(?error, "Failed to initialize SES Client")) + .ok(); + + Self { + 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<()> { + // Not using the same email client which was created at startup as the role session would expire + // Create a client every time when the email is being sent + let email_client = Self::create_client(&self.settings, proxy_url) + .await + .change_context(EmailError::ClientBuildingFailure)?; + + email_client + .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/external_services/src/file_storage.rs b/crates/external_services/src/file_storage.rs new file mode 100644 index 000000000000..fb419b6ec643 --- /dev/null +++ b/crates/external_services/src/file_storage.rs @@ -0,0 +1,96 @@ +//! +//! Module for managing file storage operations with support for multiple storage schemes. +//! + +use std::fmt::{Display, Formatter}; + +use common_utils::errors::CustomResult; + +/// Includes functionality for AWS S3 storage operations. +#[cfg(feature = "aws_s3")] +mod aws_s3; + +mod file_system; + +/// Enum representing different file storage configurations, allowing for multiple storage schemes. +#[derive(Debug, Clone, Default, serde::Deserialize)] +#[serde(tag = "file_storage_backend")] +#[serde(rename_all = "snake_case")] +pub enum FileStorageConfig { + /// AWS S3 storage configuration. + #[cfg(feature = "aws_s3")] + AwsS3 { + /// Configuration for AWS S3 file storage. + aws_s3: aws_s3::AwsFileStorageConfig, + }, + /// Local file system storage configuration. + #[default] + FileSystem, +} + +impl FileStorageConfig { + /// Validates the file storage configuration. + pub fn validate(&self) -> Result<(), InvalidFileStorageConfig> { + match self { + #[cfg(feature = "aws_s3")] + Self::AwsS3 { aws_s3 } => aws_s3.validate(), + Self::FileSystem => Ok(()), + } + } + + /// Retrieves the appropriate file storage client based on the file storage configuration. + pub async fn get_file_storage_client(&self) -> Box { + match self { + #[cfg(feature = "aws_s3")] + Self::AwsS3 { aws_s3 } => Box::new(aws_s3::AwsFileStorageClient::new(aws_s3).await), + Self::FileSystem => Box::new(file_system::FileSystem), + } + } +} + +/// Trait for file storage operations +#[async_trait::async_trait] +pub trait FileStorageInterface: dyn_clone::DynClone + Sync + Send { + /// Uploads a file to the selected storage scheme. + async fn upload_file( + &self, + file_key: &str, + file: Vec, + ) -> CustomResult<(), FileStorageError>; + + /// Deletes a file from the selected storage scheme. + async fn delete_file(&self, file_key: &str) -> CustomResult<(), FileStorageError>; + + /// Retrieves a file from the selected storage scheme. + async fn retrieve_file(&self, file_key: &str) -> CustomResult, FileStorageError>; +} + +dyn_clone::clone_trait_object!(FileStorageInterface); + +/// Error thrown when the file storage config is invalid +#[derive(Debug, Clone)] +pub struct InvalidFileStorageConfig(&'static str); + +impl std::error::Error for InvalidFileStorageConfig {} + +impl Display for InvalidFileStorageConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "file_storage: {}", self.0) + } +} + +/// Represents errors that can occur during file storage operations. +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum FileStorageError { + /// Indicates that the file upload operation failed. + #[error("Failed to upload file")] + UploadFailed, + + /// Indicates that the file retrieval operation failed. + #[error("Failed to retrieve file")] + RetrieveFailed, + + /// Indicates that the file deletion operation failed. + #[error("Failed to delete file")] + DeleteFailed, +} diff --git a/crates/external_services/src/file_storage/aws_s3.rs b/crates/external_services/src/file_storage/aws_s3.rs new file mode 100644 index 000000000000..86d1c0f0efab --- /dev/null +++ b/crates/external_services/src/file_storage/aws_s3.rs @@ -0,0 +1,158 @@ +use aws_config::meta::region::RegionProviderChain; +use aws_sdk_s3::{ + operation::{ + delete_object::DeleteObjectError, get_object::GetObjectError, put_object::PutObjectError, + }, + Client, +}; +use aws_sdk_sts::config::Region; +use common_utils::{errors::CustomResult, ext_traits::ConfigExt}; +use error_stack::ResultExt; + +use super::InvalidFileStorageConfig; +use crate::file_storage::{FileStorageError, FileStorageInterface}; + +/// Configuration for AWS S3 file storage. +#[derive(Debug, serde::Deserialize, Clone, Default)] +#[serde(default)] +pub struct AwsFileStorageConfig { + /// The AWS region to send file uploads + region: String, + /// The AWS s3 bucket to send file uploads + bucket_name: String, +} + +impl AwsFileStorageConfig { + /// Validates the AWS S3 file storage configuration. + pub(super) fn validate(&self) -> Result<(), InvalidFileStorageConfig> { + use common_utils::fp_utils::when; + + when(self.region.is_default_or_empty(), || { + Err(InvalidFileStorageConfig("aws s3 region must not be empty")) + })?; + + when(self.bucket_name.is_default_or_empty(), || { + Err(InvalidFileStorageConfig( + "aws s3 bucket name must not be empty", + )) + }) + } +} + +/// AWS S3 file storage client. +#[derive(Debug, Clone)] +pub(super) struct AwsFileStorageClient { + /// AWS S3 client + inner_client: Client, + /// The name of the AWS S3 bucket. + bucket_name: String, +} + +impl AwsFileStorageClient { + /// Creates a new AWS S3 file storage client. + pub(super) async fn new(config: &AwsFileStorageConfig) -> Self { + let region_provider = RegionProviderChain::first_try(Region::new(config.region.clone())); + let sdk_config = aws_config::from_env().region(region_provider).load().await; + Self { + inner_client: Client::new(&sdk_config), + bucket_name: config.bucket_name.clone(), + } + } + + /// Uploads a file to AWS S3. + async fn upload_file( + &self, + file_key: &str, + file: Vec, + ) -> CustomResult<(), AwsS3StorageError> { + self.inner_client + .put_object() + .bucket(&self.bucket_name) + .key(file_key) + .body(file.into()) + .send() + .await + .map_err(AwsS3StorageError::UploadFailure)?; + Ok(()) + } + + /// Deletes a file from AWS S3. + async fn delete_file(&self, file_key: &str) -> CustomResult<(), AwsS3StorageError> { + self.inner_client + .delete_object() + .bucket(&self.bucket_name) + .key(file_key) + .send() + .await + .map_err(AwsS3StorageError::DeleteFailure)?; + Ok(()) + } + + /// Retrieves a file from AWS S3. + async fn retrieve_file(&self, file_key: &str) -> CustomResult, AwsS3StorageError> { + Ok(self + .inner_client + .get_object() + .bucket(&self.bucket_name) + .key(file_key) + .send() + .await + .map_err(AwsS3StorageError::RetrieveFailure)? + .body + .collect() + .await + .map_err(AwsS3StorageError::UnknownError)? + .to_vec()) + } +} + +#[async_trait::async_trait] +impl FileStorageInterface for AwsFileStorageClient { + /// Uploads a file to AWS S3. + async fn upload_file( + &self, + file_key: &str, + file: Vec, + ) -> CustomResult<(), FileStorageError> { + self.upload_file(file_key, file) + .await + .change_context(FileStorageError::UploadFailed)?; + Ok(()) + } + + /// Deletes a file from AWS S3. + async fn delete_file(&self, file_key: &str) -> CustomResult<(), FileStorageError> { + self.delete_file(file_key) + .await + .change_context(FileStorageError::DeleteFailed)?; + Ok(()) + } + + /// Retrieves a file from AWS S3. + async fn retrieve_file(&self, file_key: &str) -> CustomResult, FileStorageError> { + Ok(self + .retrieve_file(file_key) + .await + .change_context(FileStorageError::RetrieveFailed)?) + } +} + +/// Enum representing errors that can occur during AWS S3 file storage operations. +#[derive(Debug, thiserror::Error)] +enum AwsS3StorageError { + /// Error indicating that file upload to S3 failed. + #[error("File upload to S3 failed: {0:?}")] + UploadFailure(aws_smithy_client::SdkError), + + /// Error indicating that file retrieval from S3 failed. + #[error("File retrieve from S3 failed: {0:?}")] + RetrieveFailure(aws_smithy_client::SdkError), + + /// Error indicating that file deletion from S3 failed. + #[error("File delete from S3 failed: {0:?}")] + DeleteFailure(aws_smithy_client::SdkError), + + /// Unknown error occurred. + #[error("Unknown error occurred: {0:?}")] + UnknownError(aws_sdk_s3::primitives::ByteStreamError), +} diff --git a/crates/external_services/src/file_storage/file_system.rs b/crates/external_services/src/file_storage/file_system.rs new file mode 100644 index 000000000000..15ca84deeb8e --- /dev/null +++ b/crates/external_services/src/file_storage/file_system.rs @@ -0,0 +1,144 @@ +//! +//! Module for local file system storage operations +//! + +use std::{ + fs::{remove_file, File}, + io::{Read, Write}, + path::PathBuf, +}; + +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; + +use crate::file_storage::{FileStorageError, FileStorageInterface}; + +/// Constructs the file path for a given file key within the file system. +/// The file path is generated based on the workspace path and the provided file key. +fn get_file_path(file_key: impl AsRef) -> PathBuf { + let mut file_path = PathBuf::new(); + #[cfg(feature = "logs")] + file_path.push(router_env::env::workspace_path()); + #[cfg(not(feature = "logs"))] + file_path.push(std::env::current_dir().unwrap_or(".".into())); + + file_path.push("files"); + file_path.push(file_key.as_ref()); + file_path +} + +/// Represents a file system for storing and managing files locally. +#[derive(Debug, Clone)] +pub(super) struct FileSystem; + +impl FileSystem { + /// Saves the provided file data to the file system under the specified file key. + async fn upload_file( + &self, + file_key: &str, + file: Vec, + ) -> CustomResult<(), FileSystemStorageError> { + let file_path = get_file_path(file_key); + + // Ignore the file name and create directories in the `file_path` if not exists + std::fs::create_dir_all( + file_path + .parent() + .ok_or(FileSystemStorageError::CreateDirFailed) + .into_report() + .attach_printable("Failed to obtain parent directory")?, + ) + .into_report() + .change_context(FileSystemStorageError::CreateDirFailed)?; + + let mut file_handler = File::create(file_path) + .into_report() + .change_context(FileSystemStorageError::CreateFailure)?; + file_handler + .write_all(&file) + .into_report() + .change_context(FileSystemStorageError::WriteFailure)?; + Ok(()) + } + + /// Deletes the file associated with the specified file key from the file system. + async fn delete_file(&self, file_key: &str) -> CustomResult<(), FileSystemStorageError> { + let file_path = get_file_path(file_key); + remove_file(file_path) + .into_report() + .change_context(FileSystemStorageError::DeleteFailure)?; + Ok(()) + } + + /// Retrieves the file content associated with the specified file key from the file system. + async fn retrieve_file(&self, file_key: &str) -> CustomResult, FileSystemStorageError> { + let mut received_data: Vec = Vec::new(); + let file_path = get_file_path(file_key); + let mut file = File::open(file_path) + .into_report() + .change_context(FileSystemStorageError::FileOpenFailure)?; + file.read_to_end(&mut received_data) + .into_report() + .change_context(FileSystemStorageError::ReadFailure)?; + Ok(received_data) + } +} + +#[async_trait::async_trait] +impl FileStorageInterface for FileSystem { + /// Saves the provided file data to the file system under the specified file key. + async fn upload_file( + &self, + file_key: &str, + file: Vec, + ) -> CustomResult<(), FileStorageError> { + self.upload_file(file_key, file) + .await + .change_context(FileStorageError::UploadFailed)?; + Ok(()) + } + + /// Deletes the file associated with the specified file key from the file system. + async fn delete_file(&self, file_key: &str) -> CustomResult<(), FileStorageError> { + self.delete_file(file_key) + .await + .change_context(FileStorageError::DeleteFailed)?; + Ok(()) + } + + /// Retrieves the file content associated with the specified file key from the file system. + async fn retrieve_file(&self, file_key: &str) -> CustomResult, FileStorageError> { + Ok(self + .retrieve_file(file_key) + .await + .change_context(FileStorageError::RetrieveFailed)?) + } +} + +/// Represents an error that can occur during local file system storage operations. +#[derive(Debug, thiserror::Error)] +enum FileSystemStorageError { + /// Error indicating opening a file failed + #[error("Failed while opening the file")] + FileOpenFailure, + + /// Error indicating file creation failed. + #[error("Failed to create file")] + CreateFailure, + + /// Error indicating reading a file failed. + #[error("Failed while reading the file")] + ReadFailure, + + /// Error indicating writing to a file failed. + #[error("Failed while writing into file")] + WriteFailure, + + /// Error indicating file deletion failed. + #[error("Failed while deleting the file")] + DeleteFailure, + + /// Error indicating directory creation failed + #[error("Failed while creating a directory")] + CreateDirFailed, +} diff --git a/crates/external_services/src/hashicorp_vault.rs b/crates/external_services/src/hashicorp_vault.rs new file mode 100644 index 000000000000..e31c8f01392e --- /dev/null +++ b/crates/external_services/src/hashicorp_vault.rs @@ -0,0 +1,215 @@ +//! Interactions with the HashiCorp Vault + +use std::{collections::HashMap, future::Future, pin::Pin}; + +use error_stack::{Report, ResultExt}; +use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; + +/// Utilities for supporting decryption of data +pub mod decrypt; + +static HC_CLIENT: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); + +#[allow(missing_debug_implementations)] +/// A struct representing a connection to HashiCorp Vault. +pub struct HashiCorpVault { + /// The underlying client used for interacting with HashiCorp Vault. + client: VaultClient, +} + +/// Configuration for connecting to HashiCorp Vault. +#[derive(Clone, Debug, Default, serde::Deserialize)] +#[serde(default)] +pub struct HashiCorpVaultConfig { + /// The URL of the HashiCorp Vault server. + pub url: String, + /// The authentication token used to access HashiCorp Vault. + pub token: String, +} + +/// Asynchronously retrieves a HashiCorp Vault client based on the provided configuration. +/// +/// # Parameters +/// +/// - `config`: A reference to a `HashiCorpVaultConfig` containing the configuration details. +pub async fn get_hashicorp_client( + config: &HashiCorpVaultConfig, +) -> error_stack::Result<&'static HashiCorpVault, HashiCorpError> { + HC_CLIENT + .get_or_try_init(|| async { HashiCorpVault::new(config) }) + .await +} + +/// A trait defining an engine for interacting with HashiCorp Vault. +pub trait Engine: Sized { + /// The associated type representing the return type of the engine's operations. + type ReturnType<'b, T> + where + T: 'b, + Self: 'b; + /// Reads data from HashiCorp Vault at the specified location. + /// + /// # Parameters + /// + /// - `client`: A reference to the HashiCorpVault client. + /// - `location`: The location in HashiCorp Vault to read data from. + /// + /// # Returns + /// + /// A future representing the result of the read operation. + fn read(client: &HashiCorpVault, location: String) -> Self::ReturnType<'_, String>; +} + +/// An implementation of the `Engine` trait for the Key-Value version 2 (Kv2) engine. +#[derive(Debug)] +pub enum Kv2 {} + +impl Engine for Kv2 { + type ReturnType<'b, T: 'b> = + Pin> + Send + 'b>>; + fn read(client: &HashiCorpVault, location: String) -> Self::ReturnType<'_, String> { + Box::pin(async move { + let mut split = location.split(':'); + let mount = split.next().ok_or(HashiCorpError::IncompleteData)?; + let path = split.next().ok_or(HashiCorpError::IncompleteData)?; + let key = split.next().unwrap_or("value"); + + let mut output = + vaultrs::kv2::read::>(&client.client, mount, path) + .await + .map_err(Into::>::into) + .change_context(HashiCorpError::FetchFailed)?; + + Ok(output.remove(key).ok_or(HashiCorpError::ParseError)?) + }) + } +} + +impl HashiCorpVault { + /// Creates a new instance of HashiCorpVault based on the provided configuration. + /// + /// # Parameters + /// + /// - `config`: A reference to a `HashiCorpVaultConfig` containing the configuration details. + /// + pub fn new(config: &HashiCorpVaultConfig) -> error_stack::Result { + VaultClient::new( + VaultClientSettingsBuilder::default() + .address(&config.url) + .token(&config.token) + .build() + .map_err(Into::>::into) + .change_context(HashiCorpError::ClientCreationFailed) + .attach_printable("Failed while building vault settings")?, + ) + .map_err(Into::>::into) + .change_context(HashiCorpError::ClientCreationFailed) + .map(|client| Self { client }) + } + + /// Asynchronously fetches data from HashiCorp Vault using the specified engine. + /// + /// # Parameters + /// + /// - `data`: A String representing the location or identifier of the data in HashiCorp Vault. + /// + /// # Type Parameters + /// + /// - `En`: The engine type that implements the `Engine` trait. + /// - `I`: The type that can be constructed from the retrieved encoded data. + /// + pub async fn fetch(&self, data: String) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >, + > + 'a, + I: FromEncoded, + { + let output = En::read(self, data).await?; + I::from_encoded(output).ok_or(error_stack::report!(HashiCorpError::HexDecodingFailed)) + } +} + +/// A trait for types that can be constructed from encoded data in the form of a String. +pub trait FromEncoded: Sized { + /// Constructs an instance of the type from the provided encoded input. + /// + /// # Parameters + /// + /// - `input`: A String containing the encoded data. + /// + /// # Returns + /// + /// An `Option` representing the constructed instance if successful, or `None` otherwise. + /// + /// # Example + /// + /// ```rust + /// # use your_module::{FromEncoded, masking::Secret, Vec}; + /// let secret_instance = Secret::::from_encoded("encoded_secret_string".to_string()); + /// let vec_instance = Vec::::from_encoded("68656c6c6f".to_string()); + /// ``` + fn from_encoded(input: String) -> Option; +} + +impl FromEncoded for masking::Secret { + fn from_encoded(input: String) -> Option { + Some(input.into()) + } +} + +impl FromEncoded for Vec { + fn from_encoded(input: String) -> Option { + hex::decode(input).ok() + } +} + +/// An enumeration representing various errors that can occur in interactions with HashiCorp Vault. +#[derive(Debug, thiserror::Error)] +pub enum HashiCorpError { + /// Failed while creating hashicorp client + #[error("Failed while creating a new client")] + ClientCreationFailed, + + /// Failed while building configurations for hashicorp client + #[error("Failed while building configuration")] + ConfigurationBuildFailed, + + /// Failed while decoding data to hex format + #[error("Failed while decoding hex data")] + HexDecodingFailed, + + /// An error occurred when base64 decoding input data. + #[error("Failed to base64 decode input data")] + Base64DecodingFailed, + + /// An error occurred when KMS decrypting input data. + #[error("Failed to KMS decrypt input data")] + DecryptionFailed, + + /// The KMS decrypted output does not include a plaintext output. + #[error("Missing plaintext KMS decryption output")] + MissingPlaintextDecryptionOutput, + + /// An error occurred UTF-8 decoding KMS decrypted output. + #[error("Failed to UTF-8 decode decryption output")] + Utf8DecodingFailed, + + /// Incomplete data provided to fetch data from hasicorp + #[error("Provided information about the value is incomplete")] + IncompleteData, + + /// Failed while fetching data from vault + #[error("Failed while fetching data from the server")] + FetchFailed, + + /// Failed while parsing received data + #[error("Failed while parsing the response")] + ParseError, +} diff --git a/crates/external_services/src/hashicorp_vault/decrypt.rs b/crates/external_services/src/hashicorp_vault/decrypt.rs new file mode 100644 index 000000000000..1bc1b6ffa16e --- /dev/null +++ b/crates/external_services/src/hashicorp_vault/decrypt.rs @@ -0,0 +1,50 @@ +use std::{future::Future, pin::Pin}; + +use masking::ExposeInterface; + +/// A trait for types that can be asynchronously fetched and decrypted from HashiCorp Vault. +#[async_trait::async_trait] +pub trait VaultFetch: Sized { + /// Asynchronously decrypts the inner content of the type. + /// + /// # Returns + /// + /// An `Result` representing the decrypted instance if successful, + /// or an `super::HashiCorpError` with details about the encountered error. + /// + async fn fetch_inner( + self, + client: &super::HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: super::Engine< + ReturnType<'a, String> = Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >, + > + 'a; +} + +#[async_trait::async_trait] +impl VaultFetch for masking::Secret { + async fn fetch_inner( + self, + client: &super::HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: super::Engine< + ReturnType<'a, String> = Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >, + > + 'a, + { + client.fetch::(self.expose()).await + } +} diff --git a/crates/external_services/src/kms.rs b/crates/external_services/src/kms.rs index 31c82253fe80..740bca4d821b 100644 --- a/crates/external_services/src/kms.rs +++ b/crates/external_services/src/kms.rs @@ -75,7 +75,7 @@ impl KmsClient { // Logging using `Debug` representation of the error as the `Display` // representation does not hold sufficient information. logger::error!(kms_sdk_error=?error, "Failed to KMS decrypt data"); - metrics::AWS_KMS_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::AWS_KMS_DECRYPTION_FAILURES.add(&metrics::CONTEXT, 1, &[]); error }) .into_report() @@ -96,11 +96,51 @@ impl KmsClient { Ok(output) } + + /// Encrypts the provided String data using the AWS KMS SDK. We assume that + /// the SDK has the values required to interact with the AWS KMS APIs (`AWS_ACCESS_KEY_ID` and + /// `AWS_SECRET_ACCESS_KEY`) either set in environment variables, or that the SDK is running in + /// a machine that is able to assume an IAM role. + pub async fn encrypt(&self, data: impl AsRef<[u8]>) -> CustomResult { + let start = Instant::now(); + let plaintext_blob = Blob::new(data.as_ref()); + + let encrypted_output = self + .inner_client + .encrypt() + .key_id(&self.key_id) + .plaintext(plaintext_blob) + .send() + .await + .map_err(|error| { + // Logging using `Debug` representation of the error as the `Display` + // representation does not hold sufficient information. + logger::error!(kms_sdk_error=?error, "Failed to KMS encrypt data"); + metrics::AWS_KMS_ENCRYPTION_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + .into_report() + .change_context(KmsError::EncryptionFailed)?; + + let output = encrypted_output + .ciphertext_blob + .ok_or(KmsError::MissingCiphertextEncryptionOutput) + .into_report() + .map(|blob| consts::BASE64_ENGINE.encode(blob.into_inner()))?; + let time_taken = start.elapsed(); + metrics::AWS_KMS_ENCRYPT_TIME.record(&metrics::CONTEXT, time_taken.as_secs_f64(), &[]); + + Ok(output) + } } /// Errors that could occur during KMS operations. #[derive(Debug, thiserror::Error)] pub enum KmsError { + /// An error occurred when base64 encoding input data. + #[error("Failed to base64 encode input data")] + Base64EncodingFailed, + /// An error occurred when base64 decoding input data. #[error("Failed to base64 decode input data")] Base64DecodingFailed, @@ -109,10 +149,18 @@ pub enum KmsError { #[error("Failed to KMS decrypt input data")] DecryptionFailed, + /// An error occurred when KMS encrypting input data. + #[error("Failed to KMS encrypt input data")] + EncryptionFailed, + /// The KMS decrypted output does not include a plaintext output. #[error("Missing plaintext KMS decryption output")] MissingPlaintextDecryptionOutput, + /// The KMS encrypted output does not include a ciphertext output. + #[error("Missing ciphertext KMS encryption output")] + MissingCiphertextEncryptionOutput, + /// An error occurred UTF-8 decoding KMS decrypted output. #[error("Failed to UTF-8 decode decryption output")] Utf8DecodingFailed, @@ -142,8 +190,93 @@ impl KmsConfig { #[serde(transparent)] pub struct KmsValue(Secret); +impl From for KmsValue { + fn from(value: String) -> Self { + Self(Secret::new(value)) + } +} + +impl From> for KmsValue { + fn from(value: Secret) -> Self { + Self(value) + } +} + +#[cfg(feature = "hashicorp-vault")] +#[async_trait::async_trait] +impl super::hashicorp_vault::decrypt::VaultFetch for KmsValue { + async fn fetch_inner( + self, + client: &super::hashicorp_vault::HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: super::hashicorp_vault::Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result< + String, + super::hashicorp_vault::HashiCorpError, + >, + > + Send + + 'a, + >, + >, + > + 'a, + { + self.0.fetch_inner::(client).await.map(KmsValue) + } +} + impl common_utils::ext_traits::ConfigExt for KmsValue { fn is_empty_after_trim(&self) -> bool { self.0.peek().is_empty_after_trim() } } + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + #[tokio::test] + async fn check_kms_encryption() { + std::env::set_var("AWS_SECRET_ACCESS_KEY", "YOUR SECRET ACCESS KEY"); + std::env::set_var("AWS_ACCESS_KEY_ID", "YOUR AWS ACCESS KEY ID"); + use super::*; + let config = KmsConfig { + key_id: "YOUR KMS KEY ID".to_string(), + region: "AWS REGION".to_string(), + }; + + let data = "hello".to_string(); + let binding = data.as_bytes(); + let kms_encrypted_fingerprint = KmsClient::new(&config) + .await + .encrypt(binding) + .await + .expect("kms encryption failed"); + + println!("{}", kms_encrypted_fingerprint); + } + + #[tokio::test] + async fn check_kms_decrypt() { + std::env::set_var("AWS_SECRET_ACCESS_KEY", "YOUR SECRET ACCESS KEY"); + std::env::set_var("AWS_ACCESS_KEY_ID", "YOUR AWS ACCESS KEY ID"); + use super::*; + let config = KmsConfig { + key_id: "YOUR KMS KEY ID".to_string(), + region: "AWS REGION".to_string(), + }; + + // Should decrypt to hello + let data = "KMS ENCRYPTED CIPHER".to_string(); + let binding = data.as_bytes(); + let kms_encrypted_fingerprint = KmsClient::new(&config) + .await + .decrypt(binding) + .await + .expect("kms decryption failed"); + + println!("{}", kms_encrypted_fingerprint); + } +} diff --git a/crates/external_services/src/lib.rs b/crates/external_services/src/lib.rs index fa57a0bac9c6..bba65873e91a 100644 --- a/crates/external_services/src/lib.rs +++ b/crates/external_services/src/lib.rs @@ -9,6 +9,10 @@ pub mod email; #[cfg(feature = "kms")] pub mod kms; +pub mod file_storage; +#[cfg(feature = "hashicorp-vault")] +pub mod hashicorp_vault; + /// Crate specific constants #[cfg(feature = "kms")] pub mod consts { @@ -26,8 +30,12 @@ pub mod metrics { global_meter!(GLOBAL_METER, "EXTERNAL_SERVICES"); #[cfg(feature = "kms")] - counter_metric!(AWS_KMS_FAILURES, GLOBAL_METER); // No. of AWS KMS API failures + counter_metric!(AWS_KMS_DECRYPTION_FAILURES, GLOBAL_METER); // No. of AWS KMS Decryption failures + #[cfg(feature = "kms")] + counter_metric!(AWS_KMS_ENCRYPTION_FAILURES, GLOBAL_METER); // No. of AWS KMS Encryption failures #[cfg(feature = "kms")] histogram_metric!(AWS_KMS_DECRYPT_TIME, GLOBAL_METER); // Histogram for KMS decryption time (in sec) + #[cfg(feature = "kms")] + histogram_metric!(AWS_KMS_ENCRYPT_TIME, GLOBAL_METER); // Histogram for KMS encryption time (in sec) } diff --git a/crates/kgraph_utils/Cargo.toml b/crates/kgraph_utils/Cargo.toml index cd0adf0bc8af..a07285167e4c 100644 --- a/crates/kgraph_utils/Cargo.toml +++ b/crates/kgraph_utils/Cargo.toml @@ -11,12 +11,13 @@ 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/" } # Third party crates -serde = "1.0.163" -serde_json = "1.0.96" +serde = "1.0.193" +serde_json = "1.0.108" thiserror = "1.0.43" [dev-dependencies] 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..a04e052514d6 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}, }; @@ -159,7 +156,7 @@ fn compile_request_pm_types( let or_node_neighbor_id = if amount_nodes.len() == 1 { amount_nodes - .get(0) + .first() .copied() .ok_or(KgraphError::IndexingError)? } else { @@ -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 b1636418aa17..5bcb64fd8755 100644 --- a/crates/kgraph_utils/src/transformers.rs +++ b/crates/kgraph_utils/src/transformers.rs @@ -312,12 +312,15 @@ impl IntoDirValue for api_enums::Currency { Self::ALL => Ok(dirval!(PaymentCurrency = ALL)), Self::AMD => Ok(dirval!(PaymentCurrency = AMD)), Self::ANG => Ok(dirval!(PaymentCurrency = ANG)), + Self::AOA => Ok(dirval!(PaymentCurrency = AOA)), Self::ARS => Ok(dirval!(PaymentCurrency = ARS)), Self::AUD => Ok(dirval!(PaymentCurrency = AUD)), Self::AWG => Ok(dirval!(PaymentCurrency = AWG)), Self::AZN => Ok(dirval!(PaymentCurrency = AZN)), + Self::BAM => Ok(dirval!(PaymentCurrency = BAM)), Self::BBD => Ok(dirval!(PaymentCurrency = BBD)), Self::BDT => Ok(dirval!(PaymentCurrency = BDT)), + Self::BGN => Ok(dirval!(PaymentCurrency = BGN)), Self::BHD => Ok(dirval!(PaymentCurrency = BHD)), Self::BIF => Ok(dirval!(PaymentCurrency = BIF)), Self::BMD => Ok(dirval!(PaymentCurrency = BMD)), @@ -326,6 +329,7 @@ impl IntoDirValue for api_enums::Currency { Self::BRL => Ok(dirval!(PaymentCurrency = BRL)), Self::BSD => Ok(dirval!(PaymentCurrency = BSD)), Self::BWP => Ok(dirval!(PaymentCurrency = BWP)), + Self::BYN => Ok(dirval!(PaymentCurrency = BYN)), Self::BZD => Ok(dirval!(PaymentCurrency = BZD)), Self::CAD => Ok(dirval!(PaymentCurrency = CAD)), Self::CHF => Ok(dirval!(PaymentCurrency = CHF)), @@ -334,6 +338,7 @@ impl IntoDirValue for api_enums::Currency { Self::COP => Ok(dirval!(PaymentCurrency = COP)), Self::CRC => Ok(dirval!(PaymentCurrency = CRC)), Self::CUP => Ok(dirval!(PaymentCurrency = CUP)), + Self::CVE => Ok(dirval!(PaymentCurrency = CVE)), Self::CZK => Ok(dirval!(PaymentCurrency = CZK)), Self::DJF => Ok(dirval!(PaymentCurrency = DJF)), Self::DKK => Ok(dirval!(PaymentCurrency = DKK)), @@ -343,7 +348,9 @@ impl IntoDirValue for api_enums::Currency { Self::ETB => Ok(dirval!(PaymentCurrency = ETB)), Self::EUR => Ok(dirval!(PaymentCurrency = EUR)), Self::FJD => Ok(dirval!(PaymentCurrency = FJD)), + Self::FKP => Ok(dirval!(PaymentCurrency = FKP)), Self::GBP => Ok(dirval!(PaymentCurrency = GBP)), + Self::GEL => Ok(dirval!(PaymentCurrency = GEL)), Self::GHS => Ok(dirval!(PaymentCurrency = GHS)), Self::GIP => Ok(dirval!(PaymentCurrency = GIP)), Self::GMD => Ok(dirval!(PaymentCurrency = GMD)), @@ -358,6 +365,7 @@ impl IntoDirValue for api_enums::Currency { Self::IDR => Ok(dirval!(PaymentCurrency = IDR)), Self::ILS => Ok(dirval!(PaymentCurrency = ILS)), Self::INR => Ok(dirval!(PaymentCurrency = INR)), + Self::IQD => Ok(dirval!(PaymentCurrency = IQD)), Self::JMD => Ok(dirval!(PaymentCurrency = JMD)), Self::JOD => Ok(dirval!(PaymentCurrency = JOD)), Self::JPY => Ok(dirval!(PaymentCurrency = JPY)), @@ -374,6 +382,7 @@ impl IntoDirValue for api_enums::Currency { Self::LKR => Ok(dirval!(PaymentCurrency = LKR)), Self::LRD => Ok(dirval!(PaymentCurrency = LRD)), Self::LSL => Ok(dirval!(PaymentCurrency = LSL)), + Self::LYD => Ok(dirval!(PaymentCurrency = LYD)), Self::MAD => Ok(dirval!(PaymentCurrency = MAD)), Self::MDL => Ok(dirval!(PaymentCurrency = MDL)), Self::MGA => Ok(dirval!(PaymentCurrency = MGA)), @@ -381,11 +390,13 @@ impl IntoDirValue for api_enums::Currency { Self::MMK => Ok(dirval!(PaymentCurrency = MMK)), Self::MNT => Ok(dirval!(PaymentCurrency = MNT)), Self::MOP => Ok(dirval!(PaymentCurrency = MOP)), + Self::MRU => Ok(dirval!(PaymentCurrency = MRU)), Self::MUR => Ok(dirval!(PaymentCurrency = MUR)), Self::MVR => Ok(dirval!(PaymentCurrency = MVR)), Self::MWK => Ok(dirval!(PaymentCurrency = MWK)), Self::MXN => Ok(dirval!(PaymentCurrency = MXN)), Self::MYR => Ok(dirval!(PaymentCurrency = MYR)), + Self::MZN => Ok(dirval!(PaymentCurrency = MZN)), Self::NAD => Ok(dirval!(PaymentCurrency = NAD)), Self::NGN => Ok(dirval!(PaymentCurrency = NGN)), Self::NIO => Ok(dirval!(PaymentCurrency = NIO)), @@ -393,6 +404,7 @@ impl IntoDirValue for api_enums::Currency { Self::NPR => Ok(dirval!(PaymentCurrency = NPR)), Self::NZD => Ok(dirval!(PaymentCurrency = NZD)), Self::OMR => Ok(dirval!(PaymentCurrency = OMR)), + Self::PAB => Ok(dirval!(PaymentCurrency = PAB)), Self::PEN => Ok(dirval!(PaymentCurrency = PEN)), Self::PGK => Ok(dirval!(PaymentCurrency = PGK)), Self::PHP => Ok(dirval!(PaymentCurrency = PHP)), @@ -401,33 +413,46 @@ impl IntoDirValue for api_enums::Currency { Self::PYG => Ok(dirval!(PaymentCurrency = PYG)), Self::QAR => Ok(dirval!(PaymentCurrency = QAR)), Self::RON => Ok(dirval!(PaymentCurrency = RON)), + Self::RSD => Ok(dirval!(PaymentCurrency = RSD)), Self::RUB => Ok(dirval!(PaymentCurrency = RUB)), Self::RWF => Ok(dirval!(PaymentCurrency = RWF)), Self::SAR => Ok(dirval!(PaymentCurrency = SAR)), + Self::SBD => Ok(dirval!(PaymentCurrency = SBD)), Self::SCR => Ok(dirval!(PaymentCurrency = SCR)), Self::SEK => Ok(dirval!(PaymentCurrency = SEK)), Self::SGD => Ok(dirval!(PaymentCurrency = SGD)), + Self::SHP => Ok(dirval!(PaymentCurrency = SHP)), + Self::SLE => Ok(dirval!(PaymentCurrency = SLE)), Self::SLL => Ok(dirval!(PaymentCurrency = SLL)), Self::SOS => Ok(dirval!(PaymentCurrency = SOS)), + Self::SRD => Ok(dirval!(PaymentCurrency = SRD)), Self::SSP => Ok(dirval!(PaymentCurrency = SSP)), + Self::STN => Ok(dirval!(PaymentCurrency = STN)), Self::SVC => Ok(dirval!(PaymentCurrency = SVC)), Self::SZL => Ok(dirval!(PaymentCurrency = SZL)), Self::THB => Ok(dirval!(PaymentCurrency = THB)), + Self::TND => Ok(dirval!(PaymentCurrency = TND)), + Self::TOP => Ok(dirval!(PaymentCurrency = TOP)), Self::TRY => Ok(dirval!(PaymentCurrency = TRY)), Self::TTD => Ok(dirval!(PaymentCurrency = TTD)), Self::TWD => Ok(dirval!(PaymentCurrency = TWD)), Self::TZS => Ok(dirval!(PaymentCurrency = TZS)), + Self::UAH => Ok(dirval!(PaymentCurrency = UAH)), Self::UGX => Ok(dirval!(PaymentCurrency = UGX)), Self::USD => Ok(dirval!(PaymentCurrency = USD)), Self::UYU => Ok(dirval!(PaymentCurrency = UYU)), Self::UZS => Ok(dirval!(PaymentCurrency = UZS)), + Self::VES => Ok(dirval!(PaymentCurrency = VES)), Self::VND => Ok(dirval!(PaymentCurrency = VND)), Self::VUV => Ok(dirval!(PaymentCurrency = VUV)), + Self::WST => Ok(dirval!(PaymentCurrency = WST)), Self::XAF => Ok(dirval!(PaymentCurrency = XAF)), + Self::XCD => Ok(dirval!(PaymentCurrency = XCD)), Self::XOF => Ok(dirval!(PaymentCurrency = XOF)), Self::XPF => Ok(dirval!(PaymentCurrency = XPF)), Self::YER => Ok(dirval!(PaymentCurrency = YER)), Self::ZAR => Ok(dirval!(PaymentCurrency = ZAR)), + Self::ZMW => Ok(dirval!(PaymentCurrency = ZMW)), } } } diff --git a/crates/masking/Cargo.toml b/crates/masking/Cargo.toml index 21d791642895..23f207d63d59 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 @@ -18,10 +19,11 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] bytes = { version = "1", optional = true } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time"], optional = true } +erased-serde = "0.3.31" serde = { version = "1", features = ["derive"], optional = true } -serde_json = "1.0.96" +serde_json = { version = "1.0.108", optional = true } subtle = "=2.4.1" zeroize = { version = "1.6", default-features = false } [dev-dependencies] -serde_json = "1.0.96" +serde_json = "1.0.108" diff --git a/crates/masking/src/diesel.rs b/crates/masking/src/diesel.rs index 307f083d27fb..f3576298bdb1 100644 --- a/crates/masking/src/diesel.rs +++ b/crates/masking/src/diesel.rs @@ -2,7 +2,6 @@ //! Diesel-related. //! -pub use diesel::Expression; use diesel::{ backend::Backend, deserialize::{self, FromSql, Queryable}, 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..bb81717fd670 100644 --- a/crates/masking/src/serde.rs +++ b/crates/masking/src/serde.rs @@ -2,7 +2,8 @@ //! Serde-related. //! -pub use serde::{de, ser, Deserialize, Serialize, Serializer}; +pub use erased_serde::Serialize as ErasedSerialize; +pub use serde::{de, Deserialize, Serialize, Serializer}; use serde_json::{value::Serializer as JsonValueSerializer, Value}; use crate::{Secret, Strategy, StrongSecret, ZeroizableSecret}; @@ -91,6 +92,43 @@ 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: ErasedSerialize { + /// Masked serialization. + fn masked_serialize(&self) -> Result; +} + +impl ErasedMaskSerialize for T { + fn masked_serialize(&self) -> Result { + masked_serialize(self) + } +} + +impl<'a> Serialize for dyn ErasedMaskSerialize + 'a { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + erased_serde::serialize(self, serializer) + } +} + +impl<'a> Serialize for dyn ErasedMaskSerialize + 'a + Send { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + erased_serde::serialize(self, serializer) + } +} + 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/masking/tests/basic.rs b/crates/masking/tests/basic.rs index 7857783f8304..29ba90cbeae5 100644 --- a/crates/masking/tests/basic.rs +++ b/crates/masking/tests/basic.rs @@ -117,7 +117,7 @@ fn without_serialize() -> Result<(), Box> { #[test] fn for_string() -> Result<(), Box> { - #[cfg_attr(feature = "serde", derive(Serialize))] + #[cfg_attr(all(feature = "alloc", feature = "serde"), derive(Serialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct Composite { secret_number: Secret, @@ -147,7 +147,7 @@ fn for_string() -> Result<(), Box> { // serialize - #[cfg(feature = "serde")] + #[cfg(all(feature = "alloc", feature = "serde"))] { let got = serde_json::to_string(&composite).unwrap(); let exp = r#"{"secret_number":"abc","not_secret":"not secret"}"#; diff --git a/crates/openapi/Cargo.toml b/crates/openapi/Cargo.toml new file mode 100644 index 000000000000..236916862da3 --- /dev/null +++ b/crates/openapi/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "openapi" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } +serde_json = "1.0.96" + +api_models = { version = "0.1.0", path = "../api_models", features = ["openapi"] } diff --git a/crates/openapi/src/lib.rs b/crates/openapi/src/lib.rs new file mode 100644 index 000000000000..27d67445bc2d --- /dev/null +++ b/crates/openapi/src/lib.rs @@ -0,0 +1,2 @@ +mod openapi; +pub mod routes; diff --git a/crates/openapi/src/main.rs b/crates/openapi/src/main.rs new file mode 100644 index 000000000000..88bcb8896b42 --- /dev/null +++ b/crates/openapi/src/main.rs @@ -0,0 +1,15 @@ +mod openapi; +mod routes; + +fn main() { + let file_path = "openapi/openapi_spec.json"; + #[allow(clippy::expect_used)] + std::fs::write( + file_path, + ::openapi() + .to_pretty_json() + .expect("Failed to serialize OpenAPI specification as JSON"), + ) + .expect("Failed to write OpenAPI specification to file"); + println!("Successfully saved OpenAPI specification file at '{file_path}'"); +} diff --git a/crates/router/src/openapi.rs b/crates/openapi/src/openapi.rs similarity index 62% rename from crates/router/src/openapi.rs rename to crates/openapi/src/openapi.rs index 095e1f45f93f..3d4a1893ad6d 100644 --- a/crates/router/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -1,4 +1,5 @@ -#[cfg(feature = "openapi")] +use crate::routes; + #[derive(utoipa::OpenApi)] #[openapi( info( @@ -59,94 +60,138 @@ Never share your secret api keys. Keep them guarded and secure. (name = "Customers", description = "Create and manage customers"), (name = "Payment Methods", description = "Create and manage payment methods of customers"), (name = "Disputes", description = "Manage disputes"), - // (name = "API Key", description = "Create and manage API Keys"), + (name = "API Key", description = "Create and manage API Keys"), (name = "Payouts", description = "Create and manage payouts"), (name = "payment link", description = "Create payment link"), + (name = "Routing", description = "Create and manage routing configurations"), ), + // The paths will be displayed in the same order as they are registered here paths( - crate::routes::refunds::refunds_create, - crate::routes::refunds::refunds_retrieve, - crate::routes::refunds::refunds_update, - crate::routes::refunds::refunds_list, - // Commenting this out as these are admin apis and not to be used by the merchant - // crate::routes::admin::merchant_account_create, - // 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::mandates::get_mandate, - crate::routes::mandates::revoke_mandate, - crate::routes::payments::payments_create, - // crate::routes::payments::payments_start, - crate::routes::payments::payments_retrieve, - crate::routes::payments::payments_update, - crate::routes::payments::payments_confirm, - crate::routes::payments::payments_capture, - crate::routes::payments::payments_connector_session, - // crate::routes::payments::payments_redirect_response, - crate::routes::payments::payments_cancel, - crate::routes::payments::payments_list, - crate::routes::payment_methods::create_payment_method_api, - crate::routes::payment_methods::list_payment_method_api, - crate::routes::payment_methods::list_customer_payment_method_api, - crate::routes::payment_methods::list_customer_payment_method_api_client, - crate::routes::payment_methods::payment_method_retrieve_api, - crate::routes::payment_methods::payment_method_update_api, - crate::routes::payment_methods::payment_method_delete_api, - crate::routes::customers::customers_create, - crate::routes::customers::customers_retrieve, - crate::routes::customers::customers_update, - crate::routes::customers::customers_delete, - crate::routes::customers::customers_list, - // crate::routes::api_keys::api_key_create, - // crate::routes::api_keys::api_key_retrieve, - // crate::routes::api_keys::api_key_update, - // crate::routes::api_keys::api_key_revoke, - // crate::routes::api_keys::api_key_list, - crate::routes::disputes::retrieve_disputes_list, - crate::routes::disputes::retrieve_dispute, - crate::routes::payouts::payouts_create, - crate::routes::payouts::payouts_cancel, - crate::routes::payouts::payouts_fulfill, - crate::routes::payouts::payouts_retrieve, - crate::routes::payouts::payouts_update, - 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, + // Routes for payments + routes::payments::payments_create, + routes::payments::payments_update, + routes::payments::payments_confirm, + routes::payments::payments_retrieve, + routes::payments::payments_capture, + routes::payments::payments_connector_session, + routes::payments::payments_cancel, + routes::payments::payments_list, + routes::payments::payments_incremental_authorization, + routes::payment_link::payment_link_retrieve, + + // Routes for refunds + routes::refunds::refunds_create, + routes::refunds::refunds_retrieve, + routes::refunds::refunds_update, + routes::refunds::refunds_list, + + // Routes for merchant account + routes::merchant_account::merchant_account_create, + routes::merchant_account::retrieve_merchant_account, + routes::merchant_account::update_merchant_account, + routes::merchant_account::delete_merchant_account, + + // Routes for merchant connector account + routes::merchant_connector_account::payment_connector_create, + routes::merchant_connector_account::payment_connector_retrieve, + routes::merchant_connector_account::payment_connector_list, + routes::merchant_connector_account::payment_connector_update, + routes::merchant_connector_account::payment_connector_delete, + + //Routes for gsm + routes::gsm::create_gsm_rule, + routes::gsm::get_gsm_rule, + routes::gsm::update_gsm_rule, + routes::gsm::delete_gsm_rule, + + // Routes for mandates + routes::mandates::get_mandate, + routes::mandates::revoke_mandate, + + //Routes for customers + routes::customers::customers_create, + routes::customers::customers_retrieve, + routes::customers::customers_list, + routes::customers::customers_update, + routes::customers::customers_delete, + + //Routes for payment methods + routes::payment_method::create_payment_method_api, + routes::payment_method::list_payment_method_api, + routes::payment_method::list_customer_payment_method_api, + routes::payment_method::list_customer_payment_method_api_client, + routes::payment_method::payment_method_retrieve_api, + routes::payment_method::payment_method_update_api, + routes::payment_method::payment_method_delete_api, + + // Routes for Business Profile + routes::business_profile::business_profile_create, + routes::business_profile::business_profiles_list, + routes::business_profile::business_profiles_update, + routes::business_profile::business_profiles_delete, + routes::business_profile::business_profiles_retrieve, + + // Routes for disputes + routes::disputes::retrieve_dispute, + routes::disputes::retrieve_disputes_list, + + // Routes for routing + routes::routing::routing_create_config, + routes::routing::routing_link_config, + routes::routing::routing_retrieve_config, + routes::routing::list_routing_configs, + routes::routing::routing_unlink_config, + routes::routing::routing_update_default_config, + routes::routing::routing_retrieve_default_config, + routes::routing::routing_retrieve_linked_config, + routes::routing::routing_retrieve_default_config_for_profiles, + routes::routing::routing_update_default_config_for_profile, + + // Routes for blocklist + routes::blocklist::remove_entry_from_blocklist, + routes::blocklist::list_blocked_payment_methods, + routes::blocklist::add_entry_to_blocklist, + + // Routes for payouts + routes::payouts::payouts_create, + routes::payouts::payouts_retrieve, + routes::payouts::payouts_update, + routes::payouts::payouts_cancel, + routes::payouts::payouts_fulfill, + + // Routes for api keys + routes::api_keys::api_key_create, + routes::api_keys::api_key_retrieve, + routes::api_keys::api_key_update, + routes::api_keys::api_key_revoke ), components(schemas( - crate::types::api::refunds::RefundRequest, - crate::types::api::refunds::RefundType, - crate::types::api::refunds::RefundResponse, - crate::types::api::refunds::RefundStatus, - crate::types::api::refunds::RefundUpdateRequest, - crate::types::api::admin::MerchantAccountCreate, - crate::types::api::admin::MerchantAccountUpdate, - crate::types::api::admin::MerchantAccountDeleteResponse, - crate::types::api::admin::MerchantConnectorDeleteResponse, - crate::types::api::admin::MerchantConnectorResponse, - crate::types::api::customers::CustomerRequest, - crate::types::api::customers::CustomerDeleteResponse, - crate::types::api::payment_methods::PaymentMethodCreate, - crate::types::api::payment_methods::PaymentMethodResponse, - crate::types::api::payment_methods::PaymentMethodList, - crate::types::api::payment_methods::CustomerPaymentMethod, - crate::types::api::payment_methods::PaymentMethodListResponse, - crate::types::api::payment_methods::CustomerPaymentMethodsListResponse, - crate::types::api::payment_methods::PaymentMethodDeleteResponse, - crate::types::api::payment_methods::PaymentMethodUpdate, - crate::types::api::payment_methods::CardDetailFromLocker, - crate::types::api::payment_methods::CardDetail, + api_models::refunds::RefundRequest, + api_models::refunds::RefundType, + api_models::refunds::RefundResponse, + api_models::refunds::RefundStatus, + api_models::refunds::RefundUpdateRequest, + api_models::admin::MerchantAccountCreate, + api_models::admin::MerchantAccountUpdate, + api_models::admin::MerchantAccountDeleteResponse, + api_models::admin::MerchantConnectorDeleteResponse, + api_models::admin::MerchantConnectorResponse, + api_models::customers::CustomerRequest, + api_models::customers::CustomerDeleteResponse, + api_models::payment_methods::PaymentMethodCreate, + api_models::payment_methods::PaymentMethodResponse, + api_models::payment_methods::PaymentMethodList, + api_models::payment_methods::CustomerPaymentMethod, + api_models::payment_methods::PaymentMethodListResponse, + api_models::payment_methods::CustomerPaymentMethodsListResponse, + api_models::payment_methods::PaymentMethodDeleteResponse, + api_models::payment_methods::PaymentMethodUpdate, + api_models::payment_methods::CardDetailFromLocker, + api_models::payment_methods::CardDetail, + api_models::payment_methods::RequestPaymentMethodTypes, api_models::customers::CustomerResponse, api_models::admin::AcceptedCountries, api_models::admin::AcceptedCurrencies, - api_models::enums::RoutingAlgorithm, api_models::enums::PaymentType, api_models::enums::PaymentMethod, api_models::enums::PaymentMethodType, @@ -174,6 +219,8 @@ 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::enums::AuthorizationStatus, api_models::admin::MerchantConnectorCreate, api_models::admin::MerchantConnectorUpdate, api_models::admin::PrimaryBusinessDetails, @@ -184,8 +231,11 @@ Never share your secret api keys. Keep them guarded and secure. api_models::admin::MerchantConnectorDetailsWrap, api_models::admin::MerchantConnectorDetails, api_models::admin::MerchantConnectorWebhookDetails, + api_models::admin::BusinessProfileCreate, + api_models::admin::BusinessProfileResponse, + api_models::admin::BusinessPaymentLinkConfig, + api_models::admin::PaymentLinkConfigRequest, api_models::admin::PaymentLinkConfig, - api_models::admin::PaymentLinkColorSchema, api_models::disputes::DisputeResponse, api_models::disputes::DisputeResponsePaymentsRetrieve, api_models::gsm::GsmCreateRequest, @@ -247,9 +297,12 @@ 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, + api_models::payments::PaymentsUpdateRequest, + api_models::payments::PaymentsConfirmRequest, api_models::payments::PaymentsResponse, api_models::payments::PaymentsStartRequest, api_models::payments::PaymentRetrieveBody, @@ -265,6 +318,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::SecretInfoToInitiateSdk, api_models::payments::ApplePayPaymentRequest, api_models::payments::AmountInfo, + api_models::payments::ProductType, api_models::payments::GooglePayWalletData, api_models::payments::PayPalWalletData, api_models::payments::PaypalRedirection, @@ -313,7 +367,15 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::RequestSurchargeDetails, api_models::payments::PaymentAttemptResponse, api_models::payments::CaptureResponse, + api_models::payments::PaymentsIncrementalAuthorizationRequest, + api_models::payments::IncrementalAuthorizationResponse, + api_models::payments::BrowserInformation, + api_models::payments::PaymentCreatePaymentLinkConfig, api_models::payment_methods::RequiredFieldInfo, + api_models::payment_methods::MaskedBankDetails, + api_models::payment_methods::SurchargeDetailsResponse, + api_models::payment_methods::SurchargeResponse, + api_models::payment_methods::SurchargePercentage, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::TimeRange, @@ -344,21 +406,50 @@ Never share your secret api keys. Keep them guarded and secure. api_models::webhooks::OutgoingWebhook, api_models::webhooks::OutgoingWebhookContent, api_models::enums::EventType, - crate::types::api::admin::MerchantAccountResponse, - crate::types::api::admin::MerchantConnectorId, - crate::types::api::admin::MerchantDetails, - crate::types::api::admin::WebhookDetails, - crate::types::api::api_keys::ApiKeyExpiration, - crate::types::api::api_keys::CreateApiKeyRequest, - crate::types::api::api_keys::CreateApiKeyResponse, - crate::types::api::api_keys::RetrieveApiKeyResponse, - crate::types::api::api_keys::RevokeApiKeyResponse, - crate::types::api::api_keys::UpdateApiKeyRequest, + api_models::admin::MerchantAccountResponse, + api_models::admin::MerchantConnectorId, + api_models::admin::MerchantDetails, + api_models::admin::WebhookDetails, + api_models::api_keys::ApiKeyExpiration, + api_models::api_keys::CreateApiKeyRequest, + api_models::api_keys::CreateApiKeyResponse, + api_models::api_keys::RetrieveApiKeyResponse, + api_models::api_keys::RevokeApiKeyResponse, + api_models::api_keys::UpdateApiKeyRequest, api_models::payments::RetrievePaymentLinkRequest, api_models::payments::PaymentLinkResponse, api_models::payments::RetrievePaymentLinkResponse, api_models::payments::PaymentLinkInitiateRequest, - api_models::payments::PaymentLinkObject + api_models::routing::RoutingConfigRequest, + api_models::routing::RoutingDictionaryRecord, + api_models::routing::RoutingKind, + api_models::routing::RoutableConnectorChoice, + api_models::routing::LinkedRoutingConfigRetrieveResponse, + api_models::routing::RoutingRetrieveResponse, + api_models::routing::ProfileDefaultRoutingConfig, + api_models::routing::MerchantRoutingAlgorithm, + api_models::routing::RoutingAlgorithmKind, + api_models::routing::RoutingDictionary, + api_models::routing::RoutingAlgorithm, + api_models::routing::StraightThroughAlgorithm, + api_models::routing::ConnectorVolumeSplit, + api_models::routing::ConnectorSelection, + api_models::routing::ast::RoutableChoiceKind, + api_models::enums::RoutableConnectors, + api_models::routing::ast::ProgramConnectorSelection, + api_models::routing::ast::RuleConnectorSelection, + api_models::routing::ast::IfStatement, + api_models::routing::ast::Comparison, + api_models::routing::ast::ComparisonType, + api_models::routing::ast::ValueType, + api_models::routing::ast::MetadataValue, + api_models::routing::ast::NumberComparison, + api_models::payment_methods::RequestPaymentMethodTypes, + api_models::payments::PaymentLinkStatus, + api_models::blocklist::BlocklistRequest, + api_models::blocklist::BlocklistResponse, + api_models::blocklist::ListBlocklistQuery, + api_models::enums::BlocklistDataKind )), modifiers(&SecurityAddon) )] @@ -376,8 +467,7 @@ impl utoipa::Modify for SecurityAddon { "api_key", SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::with_description( "api-key", - "API keys are the most common method of authentication and can be obtained \ - from the HyperSwitch dashboard." + "Use the API key created under your merchant account from the HyperSwitch dashboard. API key is used to authenticate API requests from your merchant server only. Don't expose this key on a website or embed it in a mobile application." ))), ), ( @@ -408,107 +498,3 @@ impl utoipa::Modify for SecurityAddon { } } } - -pub mod examples { - /// Creating the payment with minimal fields - pub const PAYMENTS_CREATE_MINIMUM_FIELDS: &str = r#"{ - "amount": 6540, - "currency": "USD", - }"#; - - /// Creating a manual capture payment - pub const PAYMENTS_CREATE_WITH_MANUAL_CAPTURE: &str = r#"{ - "amount": 6540, - "currency": "USD", - "capture_method":"manual" - }"#; - - /// Creating a payment with billing and shipping address - pub const PAYMENTS_CREATE_WITH_ADDRESS: &str = r#"{ - "amount": 6540, - "currency": "USD", - "customer": { - "id" : "cus_abcdefgh" - }, - "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" - } - } - }"#; - - /// Creating a payment with customer details - pub const PAYMENTS_CREATE_WITH_CUSTOMER_DATA: &str = r#"{ - "amount": 6540, - "currency": "USD", - "customer": { - "id":"cus_abcdefgh", - "name":"John Dough", - "phone":"9999999999", - "email":"john@example.com" - } - }"#; - - /// 3DS force payment - pub const PAYMENTS_CREATE_WITH_FORCED_3DS: &str = r#"{ - "amount": 6540, - "currency": "USD", - "authentication_type" : "three_ds" - }"#; - - /// A payment with other fields - pub const PAYMENTS_CREATE: &str = r#"{ - "amount": 6540, - "currency": "USD", - "payment_id": "abcdefghijklmnopqrstuvwxyz", - "customer": { - "id":"cus_abcdefgh", - "name":"John Dough", - "phone":"9999999999", - "email":"john@example.com" - }, - "description": "Its my first payment request", - "statement_descriptor_name": "joseph", - "statement_descriptor_suffix": "JS", - "metadata": { - "udf1": "some-value", - "udf2": "some-value" - } - }"#; - - /// Creating the payment with order details - pub const PAYMENTS_CREATE_WITH_ORDER_DETAILS: &str = r#"{ - "amount": 6540, - "currency": "USD", - "order_details": [ - { - "product_name": "Apple iPhone 15", - "quantity": 1, - "amount" : 6540 - } - ] - }"#; - - /// Creating the payment with connector metadata for noon - pub const PAYMENTS_CREATE_WITH_NOON_ORDER_CATETORY: &str = r#"{ - "amount": 6540, - "currency": "USD", - "connector_metadata": { - "noon": { - "order_category":"shoes" - } - } - }"#; -} diff --git a/crates/openapi/src/routes.rs b/crates/openapi/src/routes.rs new file mode 100644 index 000000000000..5a1822959556 --- /dev/null +++ b/crates/openapi/src/routes.rs @@ -0,0 +1,26 @@ +#![allow(unused)] + +pub mod api_keys; +pub mod blocklist; +pub mod business_profile; +pub mod customers; +pub mod disputes; +pub mod gsm; +pub mod mandates; +pub mod merchant_account; +pub mod merchant_connector_account; +pub mod payment_link; +pub mod payment_method; +pub mod payments; +pub mod payouts; +pub mod refunds; +pub mod routing; + +pub use customers::*; +pub use mandates::*; +pub use merchant_account::*; +pub use merchant_connector_account::*; +pub use payment_method::*; +pub use payments::*; +pub use refunds::*; +pub use routing::*; diff --git a/crates/openapi/src/routes/api_keys.rs b/crates/openapi/src/routes/api_keys.rs new file mode 100644 index 000000000000..2956569bfb0e --- /dev/null +++ b/crates/openapi/src/routes/api_keys.rs @@ -0,0 +1,80 @@ +/// API Key - Create +/// +/// Create a new API Key for accessing our APIs from your servers. The plaintext API Key will be +/// displayed only once on creation, so ensure you store it securely. +#[utoipa::path( + post, + path = "/api_keys/{merchant_id)", + params(("merchant_id" = String, Path, description = "The unique identifier for the merchant account")), + request_body= CreateApiKeyRequest, + responses( + (status = 200, description = "API Key created", body = CreateApiKeyResponse), + (status = 400, description = "Invalid data") + ), + tag = "API Key", + operation_id = "Create an API Key", + security(("admin_api_key" = [])) +)] +pub async fn api_key_create() {} + +/// API Key - Retrieve +/// +/// Retrieve information about the specified API Key. +#[utoipa::path( + get, + path = "/api_keys/{merchant_id}/{key_id}", + params ( + ("merchant_id" = String, Path, description = "The unique identifier for the merchant account"), + ("key_id" = String, Path, description = "The unique identifier for the API Key") + ), + responses( + (status = 200, description = "API Key retrieved", body = RetrieveApiKeyResponse), + (status = 404, description = "API Key not found") + ), + tag = "API Key", + operation_id = "Retrieve an API Key", + security(("admin_api_key" = [])) +)] +pub async fn api_key_retrieve() {} + +/// API Key - Update +/// +/// Update information for the specified API Key. +#[utoipa::path( + post, + path = "/api_keys/{merchant_id}/{key_id}", + request_body = UpdateApiKeyRequest, + params ( + ("merchant_id" = String, Path, description = "The unique identifier for the merchant account"), + ("key_id" = String, Path, description = "The unique identifier for the API Key") + ), + responses( + (status = 200, description = "API Key updated", body = RetrieveApiKeyResponse), + (status = 404, description = "API Key not found") + ), + tag = "API Key", + operation_id = "Update an API Key", + security(("admin_api_key" = [])) +)] +pub async fn api_key_update() {} + +/// API Key - Revoke +/// +/// Revoke the specified API Key. Once revoked, the API Key can no longer be used for +/// authenticating with our APIs. +#[utoipa::path( + delete, + path = "/api_keys/{merchant_id)/{key_id}", + params ( + ("merchant_id" = String, Path, description = "The unique identifier for the merchant account"), + ("key_id" = String, Path, description = "The unique identifier for the API Key") + ), + responses( + (status = 200, description = "API Key revoked", body = RevokeApiKeyResponse), + (status = 404, description = "API Key not found") + ), + tag = "API Key", + operation_id = "Revoke an API Key", + security(("admin_api_key" = [])) +)] +pub async fn api_key_revoke() {} diff --git a/crates/openapi/src/routes/blocklist.rs b/crates/openapi/src/routes/blocklist.rs new file mode 100644 index 000000000000..bf683259e081 --- /dev/null +++ b/crates/openapi/src/routes/blocklist.rs @@ -0,0 +1,43 @@ +#[utoipa::path( + post, + path = "/blocklist", + request_body = BlocklistRequest, + responses( + (status = 200, description = "Fingerprint Blocked", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "Block a Fingerprint", + security(("api_key" = [])) +)] +pub async fn add_entry_to_blocklist() {} + +#[utoipa::path( + delete, + path = "/blocklist", + request_body = BlocklistRequest, + responses( + (status = 200, description = "Fingerprint Unblocked", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "Unblock a Fingerprint", + security(("api_key" = [])) +)] +pub async fn remove_entry_from_blocklist() {} + +#[utoipa::path( + get, + path = "/blocklist", + params ( + ("data_kind" = BlocklistDataKind, Query, description = "Kind of the fingerprint list requested"), + ), + responses( + (status = 200, description = "Blocked Fingerprints", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "List Blocked fingerprints of a particular kind", + security(("api_key" = [])) +)] +pub async fn list_blocked_payment_methods() {} diff --git a/crates/openapi/src/routes/business_profile.rs b/crates/openapi/src/routes/business_profile.rs new file mode 100644 index 000000000000..57da0e59701c --- /dev/null +++ b/crates/openapi/src/routes/business_profile.rs @@ -0,0 +1,124 @@ +/// Business Profile - Create +/// +/// Creates a new *business profile* for a merchant +#[utoipa::path( + post, + path = "/account/{account_id}/business_profile", + params ( + ("account_id" = String, Path, description = "The unique identifier for the merchant account") + ), + request_body( + content = BusinessProfileCreate, + examples( + ( + "Create a business profile with minimal fields" = ( + value = json!({}) + ) + ), + ( + "Create a business profile with profile name" = ( + value = json!({ + "profile_name": "shoe_business" + }) + ) + ) + ) + ), + responses( + (status = 200, description = "Business Account Created", body = BusinessProfileResponse), + (status = 400, description = "Invalid data") + ), + tag = "Business Profile", + operation_id = "Create A Business Profile", + security(("admin_api_key" = [])) +)] +pub async fn business_profile_create() {} + +/// Business Profile - List +/// +/// Lists all the *business profiles* under a merchant +#[utoipa::path( + get, + path = "/account/{account_id}/business_profile", + params ( + ("account_id" = String, Path, description = "Merchant Identifier"), + ), + responses( + (status = 200, description = "Business profiles Retrieved", body = Vec) + ), + tag = "Business Profile", + operation_id = "List Business Profiles", + security(("api_key" = [])) +)] +pub async fn business_profiles_list() {} + +/// Business Profile - Update +/// +/// Update the *business profile* +#[utoipa::path( + post, + path = "/account/{account_id}/business_profile/{profile_id}", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("profile_id" = String, Path, description = "The unique identifier for the business profile") + ), + request_body( + content = BusinessProfileCreate, + examples( + ( + "Update business profile with profile name fields" = ( + value = json!({ + "profile_name" : "shoe_business" + }) + ) + ) + )), + responses( + (status = 200, description = "Business Profile Updated", body = BusinessProfileResponse), + (status = 400, description = "Invalid data") + ), + tag = "Business Profile", + operation_id = "Update a Business Profile", + security(("api_key" = [])) +)] +pub async fn business_profiles_update() {} + +/// Business Profile - Delete +/// +/// Delete the *business profile* +#[utoipa::path( + delete, + path = "/account/{account_id}/business_profile/{profile_id}", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("profile_id" = String, Path, description = "The unique identifier for the business profile") + ), + responses( + (status = 200, description = "Business profiles Deleted", body = bool), + (status = 400, description = "Invalid data") + ), + tag = "Business Profile", + operation_id = "Delete the Business Profile", + security(("api_key" = [])) +)] +pub async fn business_profiles_delete() {} + +/// Business Profile - Retrieve +/// +/// Retrieve existing *business profile* +#[utoipa::path( + get, + path = "/account/{account_id}/business_profile/{profile_id}", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("profile_id" = String, Path, description = "The unique identifier for the business profile") + ), + responses( + (status = 200, description = "Business Profile Updated", body = BusinessProfileResponse), + (status = 400, description = "Invalid data") + ), + tag = "Business Profile", + operation_id = "Retrieve a Business Profile", + security(("api_key" = [])) +)] +pub async fn business_profiles_retrieve() {} diff --git a/crates/openapi/src/routes/customers.rs b/crates/openapi/src/routes/customers.rs new file mode 100644 index 000000000000..7517cdc61a04 --- /dev/null +++ b/crates/openapi/src/routes/customers.rs @@ -0,0 +1,102 @@ +/// Customers - Create +/// +/// Creates a customer object and stores the customer details to be reused for future payments. +/// Incase the customer already exists in the system, this API will respond with the customer details. +#[utoipa::path( + post, + path = "/customers", + request_body ( + content = CustomerRequest, + examples (( "Update name and email of a customer" =( + value =json!( { + "email": "guest@example.com", + "name": "John Doe" + }) + ))) + ), + responses( + (status = 200, description = "Customer Created", body = CustomerResponse), + (status = 400, description = "Invalid data") + + ), + tag = "Customers", + operation_id = "Create a Customer", + security(("api_key" = [])) +)] +pub async fn customers_create() {} + +/// Customers - Retrieve +/// +/// Retrieves a customer's details. +#[utoipa::path( + get, + path = "/customers/{customer_id}", + params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), + responses( + (status = 200, description = "Customer Retrieved", body = CustomerResponse), + (status = 404, description = "Customer was not found") + ), + tag = "Customers", + operation_id = "Retrieve a Customer", + security(("api_key" = []), ("ephemeral_key" = [])) +)] +pub async fn customers_retrieve() {} + +/// Customers - Update +/// +/// Updates the customer's details in a customer object. +#[utoipa::path( + post, + path = "/customers/{customer_id}", + request_body ( + content = CustomerRequest, + examples (( "Update name and email of a customer" =( + value =json!( { + "email": "guest@example.com", + "name": "John Doe" + }) + ))) + ), + params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), + responses( + (status = 200, description = "Customer was Updated", body = CustomerResponse), + (status = 404, description = "Customer was not found") + ), + tag = "Customers", + operation_id = "Update a Customer", + security(("api_key" = [])) +)] +pub async fn customers_update() {} + +/// Customers - Delete +/// +/// Delete a customer record. +#[utoipa::path( + delete, + path = "/customers/{customer_id}", + params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), + responses( + (status = 200, description = "Customer was Deleted", body = CustomerDeleteResponse), + (status = 404, description = "Customer was not found") + ), + tag = "Customers", + operation_id = "Delete a Customer", + security(("api_key" = [])) +)] +pub async fn customers_delete() {} + +/// Customers - List +/// +/// Lists all the customers for a particular merchant id. +#[utoipa::path( + post, + path = "/customers/list", + responses( + (status = 200, description = "Customers retrieved", body = Vec), + (status = 400, description = "Invalid Data"), + ), + tag = "Customers List", + operation_id = "List all Customers for a Merchant", + security(("api_key" = [])) +)] +pub async fn customers_list() {} diff --git a/crates/openapi/src/routes/disputes.rs b/crates/openapi/src/routes/disputes.rs new file mode 100644 index 000000000000..491edae41010 --- /dev/null +++ b/crates/openapi/src/routes/disputes.rs @@ -0,0 +1,44 @@ +/// Disputes - Retrieve Dispute +/// Retrieves a dispute +#[utoipa::path( + get, + path = "/disputes/{dispute_id}", + params( + ("dispute_id" = String, Path, description = "The identifier for dispute") + ), + responses( + (status = 200, description = "The dispute was retrieved successfully", body = DisputeResponse), + (status = 404, description = "Dispute does not exist in our records") + ), + tag = "Disputes", + operation_id = "Retrieve a Dispute", + security(("api_key" = [])) +)] +pub async fn retrieve_dispute() {} + +/// Disputes - List Disputes +/// Lists all the Disputes for a merchant +#[utoipa::path( + get, + path = "/disputes/list", + params( + ("limit" = Option, Query, description = "The maximum number of Dispute Objects to include in the response"), + ("dispute_status" = Option, Query, description = "The status of dispute"), + ("dispute_stage" = Option, Query, description = "The stage of dispute"), + ("reason" = Option, Query, description = "The reason for dispute"), + ("connector" = Option, Query, description = "The connector linked to dispute"), + ("received_time" = Option, Query, description = "The time at which dispute is received"), + ("received_time.lt" = Option, Query, description = "Time less than the dispute received time"), + ("received_time.gt" = Option, Query, description = "Time greater than the dispute received time"), + ("received_time.lte" = Option, Query, description = "Time less than or equals to the dispute received time"), + ("received_time.gte" = Option, Query, description = "Time greater than or equals to the dispute received time"), + ), + responses( + (status = 200, description = "The dispute list was retrieved successfully", body = Vec), + (status = 401, description = "Unauthorized request") + ), + tag = "Disputes", + operation_id = "List Disputes", + security(("api_key" = [])) +)] +pub async fn retrieve_disputes_list() {} diff --git a/crates/openapi/src/routes/gsm.rs b/crates/openapi/src/routes/gsm.rs new file mode 100644 index 000000000000..f20cc1f261a6 --- /dev/null +++ b/crates/openapi/src/routes/gsm.rs @@ -0,0 +1,75 @@ +/// Gsm - Create +/// +/// Creates a GSM (Global Status Mapping) Rule. A GSM rule is used to map a connector's error message/error code combination during a particular payments flow/sub-flow to Hyperswitch's unified status/error code/error message combination. It is also used to decide the next action in the flow - retry/requeue/do_default +#[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" = [])), +)] +pub async fn create_gsm_rule() {} + +/// Gsm - Get +/// +/// Retrieves 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" = [])), +)] +pub async fn get_gsm_rule() {} + +/// Gsm - Update +/// +/// Updates 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" = [])), +)] +pub async fn update_gsm_rule() {} + +/// Gsm - Delete +/// +/// Deletes 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" = [])), +)] +pub async fn delete_gsm_rule() {} diff --git a/crates/openapi/src/routes/mandates.rs b/crates/openapi/src/routes/mandates.rs new file mode 100644 index 000000000000..c61e2eb2f560 --- /dev/null +++ b/crates/openapi/src/routes/mandates.rs @@ -0,0 +1,61 @@ +/// Mandates - Retrieve Mandate +/// +/// Retrieves a mandate created using the Payments/Create API +#[utoipa::path( + get, + path = "/mandates/{mandate_id}", + params( + ("mandate_id" = String, Path, description = "The identifier for mandate") + ), + responses( + (status = 200, description = "The mandate was retrieved successfully", body = MandateResponse), + (status = 404, description = "Mandate does not exist in our records") + ), + tag = "Mandates", + operation_id = "Retrieve a Mandate", + security(("api_key" = [])) +)] +pub async fn get_mandate() {} + +/// Mandates - Revoke Mandate +/// +/// Revokes a mandate created using the Payments/Create API +#[utoipa::path( + post, + path = "/mandates/revoke/{mandate_id}", + params( + ("mandate_id" = String, Path, description = "The identifier for a mandate") + ), + responses( + (status = 200, description = "The mandate was revoked successfully", body = MandateRevokedResponse), + (status = 400, description = "Mandate does not exist in our records") + ), + tag = "Mandates", + operation_id = "Revoke a Mandate", + security(("api_key" = [])) +)] +pub async fn revoke_mandate() {} + +/// Mandates - List Mandates +#[utoipa::path( + get, + path = "/mandates/list", + params( + ("limit" = Option, Query, description = "The maximum number of Mandate Objects to include in the response"), + ("mandate_status" = Option, Query, description = "The status of mandate"), + ("connector" = Option, Query, description = "The connector linked to mandate"), + ("created_time" = Option, Query, description = "The time at which mandate is created"), + ("created_time.lt" = Option, Query, description = "Time less than the mandate created time"), + ("created_time.gt" = Option, Query, description = "Time greater than the mandate created time"), + ("created_time.lte" = Option, Query, description = "Time less than or equals to the mandate created time"), + ("created_time.gte" = Option, Query, description = "Time greater than or equals to the mandate created time"), + ), + responses( + (status = 200, description = "The mandate list was retrieved successfully", body = Vec), + (status = 401, description = "Unauthorized request") + ), + tag = "Mandates", + operation_id = "List Mandates", + security(("api_key" = [])) +)] +pub async fn retrieve_mandates_list() {} diff --git a/crates/openapi/src/routes/merchant_account.rs b/crates/openapi/src/routes/merchant_account.rs new file mode 100644 index 000000000000..967e28225c2a --- /dev/null +++ b/crates/openapi/src/routes/merchant_account.rs @@ -0,0 +1,125 @@ +/// Merchant Account - Create +/// +/// Create a new account for a *merchant* and the *merchant* could be a seller or retailer or client who likes to receive and send payments. +#[utoipa::path( + post, + path = "/accounts", + request_body( + content = MerchantAccountCreate, + examples( + ( + "Create a merchant account with minimal fields" = ( + value = json!({"merchant_id": "merchant_abc"}) + ) + ), + ( + "Create a merchant account with webhook url" = ( + value = json!({ + "merchant_id": "merchant_abc", + "webhook_details" : { + "webhook_url": "https://webhook.site/a5c54f75-1f7e-4545-b781-af525b7e37a0" + } + }) + ) + ), + ( + "Create a merchant account with return url" = ( + value = json!({"merchant_id": "merchant_abc", + "return_url": "https://example.com"}) + ) + ) + ) + + ), + responses( + (status = 200, description = "Merchant Account Created", body = MerchantAccountResponse), + (status = 400, description = "Invalid data") + ), + tag = "Merchant Account", + operation_id = "Create a Merchant Account", + security(("admin_api_key" = [])) +)] +pub async fn merchant_account_create() {} + +/// Merchant Account - Retrieve +/// +/// Retrieve a *merchant* account details. +#[utoipa::path( + get, + path = "/accounts/{account_id}", + params (("account_id" = String, Path, description = "The unique identifier for the merchant account")), + responses( + (status = 200, description = "Merchant Account Retrieved", body = MerchantAccountResponse), + (status = 404, description = "Merchant account not found") + ), + tag = "Merchant Account", + operation_id = "Retrieve a Merchant Account", + security(("admin_api_key" = [])) +)] +pub async fn retrieve_merchant_account() {} + +/// Merchant Account - Update +/// +/// Updates details of an existing merchant account. Helpful in updating merchant details such as email, contact details, or other configuration details like webhook, routing algorithm etc +#[utoipa::path( + post, + path = "/accounts/{account_id}", + request_body ( + content = MerchantAccountUpdate, + examples( + ( + "Update merchant name" = ( + value = json!({ + "merchant_id": "merchant_abc", + "merchant_name": "merchant_name" + }) + ) + ), + ("Update merchant name" = ( + value = json!({ + "merchant_id": "merchant_abc", + "merchant_name": "merchant_name" + }) + )), + ("Update webhook url" = ( + value = json!({ + "merchant_id": "merchant_abc", + "webhook_details": { + "webhook_url": "https://webhook.site/a5c54f75-1f7e-4545-b781-af525b7e37a0" + } + }) + ) + ), + ("Update return url" = ( + value = json!({ + "merchant_id": "merchant_abc", + "return_url": "https://example.com" + }) + )))), + params (("account_id" = String, Path, description = "The unique identifier for the merchant account")), + responses( + (status = 200, description = "Merchant Account Updated", body = MerchantAccountResponse), + (status = 404, description = "Merchant account not found") + ), + tag = "Merchant Account", + operation_id = "Update a Merchant Account", + security(("admin_api_key" = [])) +)] +pub async fn update_merchant_account() {} + +/// Merchant Account - Delete +/// +/// Delete a *merchant* account +#[utoipa::path( + delete, + path = "/accounts/{account_id}", + params (("account_id" = String, Path, description = "The unique identifier for the merchant account")), + responses( + (status = 200, description = "Merchant Account Deleted", body = MerchantAccountDeleteResponse), + (status = 404, description = "Merchant account not found") + ), + tag = "Merchant Account", + operation_id = "Delete a Merchant Account", + security(("admin_api_key" = [])) +)] +pub async fn delete_merchant_account() {} diff --git a/crates/openapi/src/routes/merchant_connector_account.rs b/crates/openapi/src/routes/merchant_connector_account.rs new file mode 100644 index 000000000000..6f7746ccd456 --- /dev/null +++ b/crates/openapi/src/routes/merchant_connector_account.rs @@ -0,0 +1,170 @@ +/// Merchant Connector - Create +/// +/// Creates a new Merchant Connector for the merchant account. The connector could be a payment processor/facilitator/acquirer or a provider of specialized services like Fraud/Accounting etc. +#[utoipa::path( + post, + path = "/accounts/{account_id}/connectors", + request_body( + content = MerchantConnectorCreate, + examples( + ( + "Create a merchant connector account with minimal fields" = ( + value = json!({ + "connector_type": "fiz_operations", + "connector_name": "adyen", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "{{adyen-api-key}}", + "key1": "{{adyen_merchant_account}}" + } + }) + ) + ), + ( + "Create a merchant connector account under a specific business profile" = ( + value = json!({ + "connector_type": "fiz_operations", + "connector_name": "adyen", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "{{adyen-api-key}}", + "key1": "{{adyen_merchant_account}}" + }, + "profile_id": "{{profile_id}}" + }) + ) + ), + ( + "Create a merchant account with custom connector label" = ( + value = json!({ + "connector_type": "fiz_operations", + "connector_name": "adyen", + "connector_label": "EU_adyen", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "{{adyen-api-key}}", + "key1": "{{adyen_merchant_account}}" + } + }) + ) + ), + ) + ), + responses( + (status = 200, description = "Merchant Connector Created", body = MerchantConnectorResponse), + (status = 400, description = "Missing Mandatory fields"), + ), + tag = "Merchant Connector Account", + operation_id = "Create a Merchant Connector", + security(("admin_api_key" = [])) +)] +pub async fn payment_connector_create() {} + +/// Merchant Connector - Retrieve +/// +/// Retrieves details of a Connector account +#[utoipa::path( + get, + path = "/accounts/{account_id}/connectors/{connector_id}", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("connector_id" = i32, Path, description = "The unique identifier for the Merchant Connector") + ), + responses( + (status = 200, description = "Merchant Connector retrieved successfully", body = MerchantConnectorResponse), + (status = 404, description = "Merchant Connector does not exist in records"), + (status = 401, description = "Unauthorized request") + ), + tag = "Merchant Connector Account", + operation_id = "Retrieve a Merchant Connector", + security(("admin_api_key" = [])) +)] +pub async fn payment_connector_retrieve() {} + +/// Merchant Connector - List +/// +/// List Merchant Connector Details for the merchant +#[utoipa::path( + get, + path = "/accounts/{account_id}/connectors", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ), + responses( + (status = 200, description = "Merchant Connector list retrieved successfully", body = Vec), + (status = 404, description = "Merchant Connector does not exist in records"), + (status = 401, description = "Unauthorized request") + ), + tag = "Merchant Connector Account", + operation_id = "List all Merchant Connectors", + security(("admin_api_key" = [])) +)] +pub async fn payment_connector_list() {} + +/// Merchant Connector - Update +/// +/// To update an existing Merchant Connector account. Helpful in enabling/disabling different payment methods and other settings for the connector +#[utoipa::path( + post, + path = "/accounts/{account_id}/connectors/{connector_id}", + request_body( + content = MerchantConnectorUpdate, + examples( + ( + "Enable card payment method" = ( + value = json! ({ + "connector_type": "fiz_operations", + "payment_methods_enabled": [ + { + "payment_method": "card" + } + ] + }) + ) + ), + ( + "Update webhook secret" = ( + value = json! ({ + "connector_webhook_details": { + "merchant_secret": "{{webhook_secret}}" + } + }) + ) + ) + ), + ), + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("connector_id" = i32, Path, description = "The unique identifier for the Merchant Connector") + ), + responses( + (status = 200, description = "Merchant Connector Updated", body = MerchantConnectorResponse), + (status = 404, description = "Merchant Connector does not exist in records"), + (status = 401, description = "Unauthorized request") + ), + tag = "Merchant Connector Account", + operation_id = "Update a Merchant Connector", + security(("admin_api_key" = [])) +)] +pub async fn payment_connector_update() {} + +/// Merchant Connector - Delete +/// +/// Delete or Detach a Merchant Connector from Merchant Account +#[utoipa::path( + delete, + path = "/accounts/{account_id}/connectors/{connector_id}", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("connector_id" = i32, Path, description = "The unique identifier for the Merchant Connector") + ), + responses( + (status = 200, description = "Merchant Connector Deleted", body = MerchantConnectorDeleteResponse), + (status = 404, description = "Merchant Connector does not exist in records"), + (status = 401, description = "Unauthorized request") + ), + tag = "Merchant Connector Account", + operation_id = "Delete a Merchant Connector", + security(("admin_api_key" = [])) +)] +pub async fn payment_connector_delete() {} diff --git a/crates/openapi/src/routes/payment_link.rs b/crates/openapi/src/routes/payment_link.rs new file mode 100644 index 000000000000..9b9cb3a4d529 --- /dev/null +++ b/crates/openapi/src/routes/payment_link.rs @@ -0,0 +1,19 @@ +/// Payments Link - Retrieve +/// +/// To retrieve the properties of a Payment Link. This may be used to get the status of a previously initiated payment or next action for an ongoing payment +#[utoipa::path( + get, + path = "/payment_link/{payment_link_id}", + params( + ("payment_link_id" = String, Path, description = "The identifier for payment link") + ), + request_body=RetrievePaymentLinkRequest, + responses( + (status = 200, description = "Gets details regarding payment link", body = RetrievePaymentLinkResponse), + (status = 404, description = "No payment link found") + ), + tag = "Payments", + operation_id = "Retrieve a Payment Link", + security(("api_key" = []), ("publishable_key" = [])) +)] +pub async fn payment_link_retrieve() {} diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs new file mode 100644 index 000000000000..ffb9d8c5f7e5 --- /dev/null +++ b/crates/openapi/src/routes/payment_method.rs @@ -0,0 +1,173 @@ +/// PaymentMethods - Create +/// +/// Creates and stores a payment method against a customer. +/// In case of cards, this API should be used only by PCI compliant merchants. +#[utoipa::path( + post, + path = "/payment_methods", + request_body ( + content = PaymentMethodCreate, + examples (( "Save a card" =( + value =json!( { + "payment_method": "card", + "payment_method_type": "credit", + "payment_method_issuer": "Visa", + "card": { + "card_number": "4242424242424242", + "card_exp_month": "11", + "card_exp_year": "25", + "card_holder_name": "John Doe" + }, + "customer_id": "{{customer_id}}" + }) + ))) + ), + responses( + (status = 200, description = "Payment Method Created", body = PaymentMethodResponse), + (status = 400, description = "Invalid Data") + + ), + tag = "Payment Methods", + operation_id = "Create a Payment Method", + security(("api_key" = [])) +)] +pub async fn create_payment_method_api() {} + +/// List payment methods for a Merchant +/// +/// Lists the applicable payment methods for a particular Merchant ID. +/// Use the client secret and publishable key authorization to list all relevant payment methods of the merchant for the payment corresponding to the client secret. +#[utoipa::path( + get, + path = "/account/payment_methods", + params ( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), + ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), + ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), + ("maximum_amount" = i64, Query, description = "The maximum amount accepted for processing by the particular payment method."), + ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), + ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), + ), + responses( + (status = 200, description = "Payment Methods retrieved", body = PaymentMethodListResponse), + (status = 400, description = "Invalid Data"), + (status = 404, description = "Payment Methods does not exist in records") + ), + tag = "Payment Methods", + operation_id = "List all Payment Methods for a Merchant", + security(("api_key" = []), ("publishable_key" = [])) +)] +pub async fn list_payment_method_api() {} + +/// List payment methods for a Customer +/// +/// Lists all the applicable payment methods for a particular Customer ID. +#[utoipa::path( + get, + path = "/customers/{customer_id}/payment_methods", + params ( + ("customer_id" = String, Path, description = "The unique identifier for the customer account"), + ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), + ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), + ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), + ("maximum_amount" = i64, Query, description = "The maximum amount accepted for processing by the particular payment method."), + ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), + ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), + ), + responses( + (status = 200, description = "Payment Methods retrieved", body = CustomerPaymentMethodsListResponse), + (status = 400, description = "Invalid Data"), + (status = 404, description = "Payment Methods does not exist in records") + ), + tag = "Payment Methods", + operation_id = "List all Payment Methods for a Customer", + security(("api_key" = [])) +)] +pub async fn list_customer_payment_method_api() {} + +/// List payment methods for a Payment +/// +/// Lists all the applicable payment methods for a particular payment tied to the `client_secret`. +#[utoipa::path( + get, + path = "/customers/payment_methods", + params ( + ("client-secret" = String, Path, description = "A secret known only to your client and the authorization server. Used for client side authentication"), + ("customer_id" = String, Path, description = "The unique identifier for the customer account"), + ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), + ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), + ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), + ("maximum_amount" = i64, Query, description = "The maximum amount accepted for processing by the particular payment method."), + ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), + ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), + ), + responses( + (status = 200, description = "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", body = CustomerPaymentMethodsListResponse), + (status = 400, description = "Invalid Data"), + (status = 404, description = "Payment Methods does not exist in records") + ), + tag = "Payment Methods", + operation_id = "List all Payment Methods for a Customer", + security(("publishable_key" = [])) +)] +pub async fn list_customer_payment_method_api_client() {} + +/// Payment Method - Retrieve +/// +/// Retrieves a payment method of a customer. +#[utoipa::path( + get, + path = "/payment_methods/{method_id}", + params ( + ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), + ), + responses( + (status = 200, description = "Payment Method retrieved", body = PaymentMethodResponse), + (status = 404, description = "Payment Method does not exist in records") + ), + tag = "Payment Methods", + operation_id = "Retrieve a Payment method", + security(("api_key" = [])) +)] +pub async fn payment_method_retrieve_api() {} + +/// Payment Method - Update +/// +/// Update an existing payment method of a customer. +/// This API is useful for use cases such as updating the card number for expired cards to prevent discontinuity in recurring payments. +#[utoipa::path( + post, + path = "/payment_methods/{method_id}", + params ( + ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), + ), + request_body = PaymentMethodUpdate, + responses( + (status = 200, description = "Payment Method updated", body = PaymentMethodResponse), + (status = 404, description = "Payment Method does not exist in records") + ), + tag = "Payment Methods", + operation_id = "Update a Payment method", + security(("api_key" = [])) +)] +pub async fn payment_method_update_api() {} + +/// Payment Method - Delete +/// +/// Deletes a payment method of a customer. +#[utoipa::path( + delete, + path = "/payment_methods/{method_id}", + params ( + ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), + ), + responses( + (status = 200, description = "Payment Method deleted", body = PaymentMethodDeleteResponse), + (status = 404, description = "Payment Method does not exist in records") + ), + tag = "Payment Methods", + operation_id = "Delete a Payment method", + security(("api_key" = [])) +)] +pub async fn payment_method_delete_api() {} diff --git a/crates/openapi/src/routes/payments.rs b/crates/openapi/src/routes/payments.rs new file mode 100644 index 000000000000..b6c83d5aae7c --- /dev/null +++ b/crates/openapi/src/routes/payments.rs @@ -0,0 +1,452 @@ +/// Payments - Create +/// +/// **Creates a payment object when amount and currency are passed.** This API is also used to create a mandate by passing the `mandate_object`. +/// +/// To completely process a payment you will have to create a payment, attach a payment method, confirm and capture funds. +/// +/// Depending on the user journey you wish to achieve, you may opt to complete all the steps in a single request by attaching a payment method, setting `confirm=true` and `capture_method = automatic` in the *Payments/Create API* request or you could use the following sequence of API requests to achieve the same: +/// +/// 1. Payments - Create +/// +/// 2. Payments - Update +/// +/// 3. Payments - Confirm +/// +/// 4. Payments - Capture. +/// +/// Use the client secret returned in this API along with your publishable key to make subsequent API calls from your client +#[utoipa::path( + post, + path = "/payments", + request_body( + content = PaymentsCreateRequest, + examples( + ( + "Create a payment with minimal fields" = ( + value = json!({"amount": 6540,"currency": "USD"}) + ) + ), + ( + "Create a payment with customer details and metadata" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "payment_id": "abcdefghijklmnopqrstuvwxyz", + "customer": { + "id": "cus_abcdefgh", + "name": "John Dough", + "phone": "9999999999", + "email": "john@example.com" + }, + "description": "Its my first payment request", + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "some-value", + "udf2": "some-value" + } + }) + ) + ), + ( + "Create a 3DS payment" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "authentication_type": "three_ds" + }) + ) + ), + ( + "Create a manual capture payment" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "capture_method": "manual" + }) + ) + ), + ( + "Create a setup mandate payment" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "confirm": true, + "customer_id": "StripeCustomer123", + "authentication_type": "no_three_ds", + "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" + } + }, + "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": 6540, + "currency": "USD" + } + } + } + }) + ) + ), + ( + "Create a recurring payment with mandate_id" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "confirm": true, + "customer_id": "StripeCustomer", + "authentication_type": "no_three_ds", + "mandate_id": "{{mandate_id}}", + "off_session": true + }) + ) + ), + ( + "Create a payment and save the card" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "confirm": true, + "customer_id": "StripeCustomer123", + "authentication_type": "no_three_ds", + "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" + } + }, + "setup_future_usage": "off_session" + }) + ) + ), + ( + "Create a payment using an already saved card's token" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "confirm": true, + "client_secret": "{{client_secret}}", + "payment_method": "card", + "payment_token": "{{payment_token}}", + "card_cvc": "123" + }) + ) + ), + ( + "Create a manual capture payment" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "customer": { + "id": "cus_abcdefgh" + }, + "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" + } + } + }) + ) + ) + ), + ), + responses( + (status = 200, description = "Payment created", body = PaymentsResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payments", + operation_id = "Create a Payment", + security(("api_key" = [])), +)] +pub fn payments_create() {} + +/// Payments - Retrieve +/// +/// Retrieves a Payment. This API can also be used to get the status of a previously initiated payment or next action for an ongoing payment +#[utoipa::path( + get, + path = "/payments/{payment_id}", + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + request_body=PaymentRetrieveBody, + responses( + (status = 200, description = "Gets the payment with final status", body = PaymentsResponse), + (status = 404, description = "No payment found") + ), + tag = "Payments", + operation_id = "Retrieve a Payment", + security(("api_key" = []), ("publishable_key" = [])) +)] +pub fn payments_retrieve() {} + +/// Payments - Update +/// +/// To update the properties of a *PaymentIntent* object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created +#[utoipa::path( + post, + path = "/payments/{payment_id}", + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + request_body( + content = PaymentsUpdateRequest, + examples( + ( + "Update the payment amount" = ( + value = json!({ + "amount": 7654, + } + ) + ) + ), + ( + "Update the shipping address" = ( + value = json!( + { + "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" + } + }, + } + ) + ) + ) + ) + ), + responses( + (status = 200, description = "Payment updated", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Update a Payment", + security(("api_key" = []), ("publishable_key" = [])) +)] +pub fn payments_update() {} + +/// Payments - Confirm +/// +/// **Use this API to confirm the payment and forward the payment to the payment processor.** +/// +/// Alternatively you can confirm the payment within the *Payments/Create* API by setting `confirm=true`. After confirmation, the payment could either: +/// +/// 1. fail with `failed` status or +/// +/// 2. transition to a `requires_customer_action` status with a `next_action` block or +/// +/// 3. succeed with either `succeeded` in case of automatic capture or `requires_capture` in case of manual capture +#[utoipa::path( + post, + path = "/payments/{payment_id}/confirm", + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + request_body( + content = PaymentsConfirmRequest, + examples( + ( + "Confirm a payment with payment method data" = ( + value = json!({ + "payment_method": "card", + "payment_method_type": "credit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + } + } + ) + ) + ) + ) + ), + responses( + (status = 200, description = "Payment confirmed", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Confirm a Payment", + security(("api_key" = []), ("publishable_key" = [])) +)] +pub fn payments_confirm() {} + +/// Payments - Capture +/// +/// To capture the funds for an uncaptured payment +#[utoipa::path( + post, + path = "/payments/{payment_id}/capture", + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + request_body ( + content = PaymentsCaptureRequest, + examples( + ( + "Capture the full amount" = ( + value = json!({}) + ) + ), + ( + "Capture partial amount" = ( + value = json!({"amount_to_capture": 654}) + ) + ), + ) + ), + responses( + (status = 200, description = "Payment captured", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Capture a Payment", + security(("api_key" = [])) +)] +pub fn payments_capture() {} + +/// Payments - Session token +/// +/// Creates a session object or a session token for wallets like Apple Pay, Google Pay, etc. These tokens are used by Hyperswitch's SDK to initiate these wallets' SDK. +#[utoipa::path( + post, + path = "/payments/session_tokens", + request_body=PaymentsSessionRequest, + responses( + (status = 200, description = "Payment session object created or session token was retrieved from wallets", body = PaymentsSessionResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Create Session tokens for a Payment", + security(("publishable_key" = [])) +)] +pub fn payments_connector_session() {} + +/// Payments - Cancel +/// +/// A Payment could can be cancelled when it is in one of these statuses: `requires_payment_method`, `requires_capture`, `requires_confirmation`, `requires_customer_action`. +#[utoipa::path( + post, + path = "/payments/{payment_id}/cancel", + request_body ( + content = PaymentsCancelRequest, + examples( + ( + "Cancel the payment with minimal fields" = ( + value = json!({}) + ) + ), + ( + "Cancel the payment with cancellation reason" = ( + value = json!({"cancellation_reason": "requested_by_customer"}) + ) + ), + ) + ), + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + responses( + (status = 200, description = "Payment canceled"), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Cancel a Payment", + security(("api_key" = [])) +)] +pub fn payments_cancel() {} + +/// Payments - List +/// +/// To list the *payments* +#[utoipa::path( + get, + path = "/payments/list", + params( + ("customer_id" = String, Query, description = "The identifier for the customer"), + ("starting_after" = String, Query, description = "A cursor for use in pagination, fetch the next list after some object"), + ("ending_before" = String, Query, description = "A cursor for use in pagination, fetch the previous list before some object"), + ("limit" = i64, Query, description = "Limit on the number of objects to return"), + ("created" = PrimitiveDateTime, Query, description = "The time at which payment is created"), + ("created_lt" = PrimitiveDateTime, Query, description = "Time less than the payment created time"), + ("created_gt" = PrimitiveDateTime, Query, description = "Time greater than the payment created time"), + ("created_lte" = PrimitiveDateTime, Query, description = "Time less than or equals to the payment created time"), + ("created_gte" = PrimitiveDateTime, Query, description = "Time greater than or equals to the payment created time") + ), + responses( + (status = 200, description = "Successfully retrieved a payment list", body = Vec), + (status = 404, description = "No payments found") + ), + tag = "Payments", + operation_id = "List all Payments", + security(("api_key" = [])) +)] +pub fn payments_list() {} + +/// Payments - Incremental Authorization +/// +/// Authorized amount for a payment can be incremented if it is in status: requires_capture +#[utoipa::path( + post, + path = "/payments/{payment_id}/incremental_authorization", + request_body=PaymentsIncrementalAuthorizationRequest, + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + responses( + (status = 200, description = "Payment authorized amount incremented", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Increment authorized amount for a Payment", + security(("api_key" = [])) +)] +pub fn payments_incremental_authorization() {} diff --git a/crates/openapi/src/routes/payouts.rs b/crates/openapi/src/routes/payouts.rs new file mode 100644 index 000000000000..3cc01de9823f --- /dev/null +++ b/crates/openapi/src/routes/payouts.rs @@ -0,0 +1,85 @@ +/// Payouts - Create +#[utoipa::path( + post, + path = "/payouts/create", + request_body=PayoutCreateRequest, + responses( + (status = 200, description = "Payout created", body = PayoutCreateResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payouts", + operation_id = "Create a Payout", + security(("api_key" = [])) +)] +pub async fn payouts_create() {} + +/// Payouts - Retrieve +#[utoipa::path( + get, + path = "/payouts/{payout_id}", + params( + ("payout_id" = String, Path, description = "The identifier for payout]") + ), + responses( + (status = 200, description = "Payout retrieved", body = PayoutCreateResponse), + (status = 404, description = "Payout does not exist in our records") + ), + tag = "Payouts", + operation_id = "Retrieve a Payout", + security(("api_key" = [])) +)] +pub async fn payouts_retrieve() {} + +/// Payouts - Update +#[utoipa::path( + post, + path = "/payouts/{payout_id}", + params( + ("payout_id" = String, Path, description = "The identifier for payout]") + ), + request_body=PayoutCreateRequest, + responses( + (status = 200, description = "Payout updated", body = PayoutCreateResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payouts", + operation_id = "Update a Payout", + security(("api_key" = [])) +)] +pub async fn payouts_update() {} + +/// Payouts - Cancel +#[utoipa::path( + post, + path = "/payouts/{payout_id}/cancel", + params( + ("payout_id" = String, Path, description = "The identifier for payout") + ), + request_body=PayoutActionRequest, + responses( + (status = 200, description = "Payout cancelled", body = PayoutCreateResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payouts", + operation_id = "Cancel a Payout", + security(("api_key" = [])) +)] +pub async fn payouts_cancel() {} + +/// Payouts - Fulfill +#[utoipa::path( + post, + path = "/payouts/{payout_id}/fulfill", + params( + ("payout_id" = String, Path, description = "The identifier for payout") + ), + request_body=PayoutActionRequest, + responses( + (status = 200, description = "Payout fulfilled", body = PayoutCreateResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payouts", + operation_id = "Fulfill a Payout", + security(("api_key" = [])) +)] +pub async fn payouts_fulfill() {} diff --git a/crates/openapi/src/routes/refunds.rs b/crates/openapi/src/routes/refunds.rs new file mode 100644 index 000000000000..aa4728869be9 --- /dev/null +++ b/crates/openapi/src/routes/refunds.rs @@ -0,0 +1,145 @@ +/// Refunds - Create +/// +/// Creates a refund against an already processed payment. In case of some processors, you can even opt to refund only a partial amount multiple times until the original charge amount has been refunded +#[utoipa::path( + post, + path = "/refunds", + request_body( + content = RefundRequest, + examples( + ( + "Create an instant refund to refund the whole amount" = ( + value = json!({ + "payment_id": "{{payment_id}}", + "refund_type": "instant" + }) + ) + ), + ( + "Create an instant refund to refund partial amount" = ( + value = json!({ + "payment_id": "{{payment_id}}", + "refund_type": "instant", + "amount": 654 + }) + ) + ), + ( + "Create an instant refund with reason" = ( + value = json!({ + "payment_id": "{{payment_id}}", + "refund_type": "instant", + "amount": 6540, + "reason": "Customer returned product" + }) + ) + ), + ) + ), + responses( + (status = 200, description = "Refund created", body = RefundResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Refunds", + operation_id = "Create a Refund", + security(("api_key" = [])) +)] +pub async fn refunds_create() {} + +/// Refunds - Retrieve +/// +/// Retrieves a Refund. This may be used to get the status of a previously initiated refund +#[utoipa::path( + get, + path = "/refunds/{refund_id}", + params( + ("refund_id" = String, Path, description = "The identifier for refund") + ), + responses( + (status = 200, description = "Refund retrieved", body = RefundResponse), + (status = 404, description = "Refund does not exist in our records") + ), + tag = "Refunds", + operation_id = "Retrieve a Refund", + security(("api_key" = [])) +)] +pub async fn refunds_retrieve() {} + +/// Refunds - Retrieve (POST) +/// +/// 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 +#[utoipa::path( + get, + path = "/refunds/sync", + responses( + (status = 200, description = "Refund retrieved", body = RefundResponse), + (status = 404, description = "Refund does not exist in our records") + ), + tag = "Refunds", + operation_id = "Retrieve a Refund", + security(("api_key" = [])) +)] +pub async fn refunds_retrieve_with_body() {} + +/// Refunds - Update +/// +/// Updates the properties of a Refund object. This API can be used to attach a reason for the refund or metadata fields +#[utoipa::path( + post, + path = "/refunds/{refund_id}", + params( + ("refund_id" = String, Path, description = "The identifier for refund") + ), + request_body( + content = RefundUpdateRequest, + examples( + ( + "Update refund reason" = ( + value = json!({ + "reason": "Paid by mistake" + }) + ) + ), + ) + ), + responses( + (status = 200, description = "Refund updated", body = RefundResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Refunds", + operation_id = "Update a Refund", + security(("api_key" = [])) +)] +pub async fn refunds_update() {} + +/// Refunds - List +/// +/// Lists all the refunds associated with the merchant or a payment_id if payment_id is not provided +#[utoipa::path( + post, + path = "/refunds/list", + request_body=RefundListRequest, + responses( + (status = 200, description = "List of refunds", body = RefundListResponse), + ), + tag = "Refunds", + operation_id = "List all Refunds", + security(("api_key" = [])) +)] +pub fn refunds_list() {} + +/// Refunds - Filter +/// +/// To list the refunds filters associated with list of connectors, currencies and payment statuses +#[utoipa::path( + post, + path = "/refunds/filter", + request_body=TimeRange, + responses( + (status = 200, description = "List of filters", body = RefundListMetaData), + ), + tag = "Refunds", + operation_id = "List all filters for Refunds", + security(("api_key" = [])) +)] +pub async fn refunds_filter_list() {} diff --git a/crates/openapi/src/routes/routing.rs b/crates/openapi/src/routes/routing.rs new file mode 100644 index 000000000000..ecfac39f9e9a --- /dev/null +++ b/crates/openapi/src/routes/routing.rs @@ -0,0 +1,202 @@ +/// Routing - Create +/// +/// Create a routing config +#[utoipa::path( + post, + path = "/routing", + request_body = RoutingConfigRequest, + responses( + (status = 200, description = "Routing config created", body = RoutingDictionaryRecord), + (status = 400, description = "Request body is malformed"), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 422, description = "Unprocessable request"), + (status = 403, description = "Forbidden"), + ), + tag = "Routing", + operation_id = "Create a routing config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_create_config() {} + +/// Routing - Activate config +/// +/// Activate a routing config +#[utoipa::path( + post, + path = "/routing/{algorithm_id}/activate", + params( + ("algorithm_id" = String, Path, description = "The unique identifier for a config"), + ), + responses( + (status = 200, description = "Routing config activated", body = RoutingDictionaryRecord), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 400, description = "Bad request") + ), + tag = "Routing", + operation_id = "Activate a routing config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_link_config() {} + +/// Routing - Retrieve +/// +/// Retrieve a routing algorithm + +#[utoipa::path( + get, + path = "/routing/{algorithm_id}", + params( + ("algorithm_id" = String, Path, description = "The unique identifier for a config"), + ), + responses( + (status = 200, description = "Successfully fetched routing config", body = MerchantRoutingAlgorithm), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 403, description = "Forbidden") + ), + tag = "Routing", + operation_id = "Retrieve a routing config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_retrieve_config() {} + +/// Routing - List +/// +/// List all routing configs +#[utoipa::path( + get, + path = "/routing", + params( + ("limit" = Option, Query, description = "The number of records to be returned"), + ("offset" = Option, Query, description = "The record offset from which to start gathering of results"), + ("profile_id" = Option, Query, description = "The unique identifier for a merchant profile"), + ), + responses( + (status = 200, description = "Successfully fetched routing configs", body = RoutingKind), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing") + ), + tag = "Routing", + operation_id = "List routing configs", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn list_routing_configs() {} + +/// Routing - Deactivate +/// +/// Deactivates a routing config +#[utoipa::path( + post, + path = "/routing/deactivate", + request_body = RoutingConfigRequest, + responses( + (status = 200, description = "Successfully deactivated routing config", body = RoutingDictionaryRecord), + (status = 500, description = "Internal server error"), + (status = 400, description = "Malformed request"), + (status = 403, description = "Malformed request"), + (status = 422, description = "Unprocessable request") + ), + tag = "Routing", + operation_id = "Deactivate a routing config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_unlink_config() {} + +/// Routing - Update Default Config +/// +/// Update default fallback config +#[utoipa::path( + post, + path = "/routing/default", + request_body = Vec, + responses( + (status = 200, description = "Successfully updated default config", body = Vec), + (status = 500, description = "Internal server error"), + (status = 400, description = "Malformed request"), + (status = 422, description = "Unprocessable request") + ), + tag = "Routing", + operation_id = "Update default fallback config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_update_default_config() {} + +/// Routing - Retrieve Default Config +/// +/// Retrieve default fallback config +#[utoipa::path( + get, + path = "/routing/default", + responses( + (status = 200, description = "Successfully retrieved default config", body = Vec), + (status = 500, description = "Internal server error") + ), + tag = "Routing", + operation_id = "Retrieve default fallback config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_retrieve_default_config() {} + +/// Routing - Retrieve Config +/// +/// Retrieve active config +#[utoipa::path( + get, + path = "/routing/active", + params( + ("profile_id" = Option, Query, description = "The unique identifier for a merchant profile"), + ), + responses( + (status = 200, description = "Successfully retrieved active config", body = LinkedRoutingConfigRetrieveResponse), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 403, description = "Forbidden") + ), + tag = "Routing", + operation_id = "Retrieve active config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_retrieve_linked_config() {} + +/// Routing - Retrieve Default For Profile +/// +/// Retrieve default config for profiles +#[utoipa::path( + get, + path = "/routing/default/profile", + responses( + (status = 200, description = "Successfully retrieved default config", body = ProfileDefaultRoutingConfig), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing") + ), + tag = "Routing", + operation_id = "Retrieve default configs for all profiles", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_retrieve_default_config_for_profiles() {} + +/// Routing - Update Default For Profile +/// +/// Update default config for profiles +#[utoipa::path( + post, + path = "/routing/default/profile/{profile_id}", + request_body = Vec, + params( + ("profile_id" = String, Path, description = "The unique identifier for a profile"), + ), + responses( + (status = 200, description = "Successfully updated default config for profile", body = ProfileDefaultRoutingConfig), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 400, description = "Malformed request"), + (status = 422, description = "Unprocessable request"), + (status = 403, description = "Forbidden"), + ), + tag = "Routing", + operation_id = "Update default configs for all profiles", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_update_default_config_for_profile() {} diff --git a/crates/pm_auth/Cargo.toml b/crates/pm_auth/Cargo.toml new file mode 100644 index 000000000000..9654932d5ef0 --- /dev/null +++ b/crates/pm_auth/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pm_auth" +description = "Open banking services" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +readme = "README.md" + +[dependencies] +# First party crates +api_models = { version = "0.1.0", path = "../api_models" } +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" } +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"] } + +# Third party crates +async-trait = "0.1.66" +bytes = "1.4.0" +error-stack = "0.3.1" +http = "0.2.9" +mime = "0.3.17" +serde = "1.0.193" +serde_json = "1.0.108" +strum = { version = "0.24.1", features = ["derive"] } +thiserror = "1.0.43" diff --git a/crates/pm_auth/README.md b/crates/pm_auth/README.md new file mode 100644 index 000000000000..c630a7fe6761 --- /dev/null +++ b/crates/pm_auth/README.md @@ -0,0 +1,3 @@ +# Payment Method Auth Services + +An open banking services for payment method auth validation diff --git a/crates/pm_auth/src/connector.rs b/crates/pm_auth/src/connector.rs new file mode 100644 index 000000000000..56aad846e248 --- /dev/null +++ b/crates/pm_auth/src/connector.rs @@ -0,0 +1,3 @@ +pub mod plaid; + +pub use self::plaid::Plaid; diff --git a/crates/pm_auth/src/connector/plaid.rs b/crates/pm_auth/src/connector/plaid.rs new file mode 100644 index 000000000000..dfcfbb7eddcc --- /dev/null +++ b/crates/pm_auth/src/connector/plaid.rs @@ -0,0 +1,340 @@ +pub mod transformers; + +use std::fmt::Debug; + +use common_utils::{ + ext_traits::BytesExt, + request::{Method, Request, RequestBuilder, RequestContent}, +}; +use error_stack::ResultExt; +use masking::{Mask, Maskable}; +use transformers as plaid; + +use crate::{ + core::errors, + types::{ + self as auth_types, + api::{ + auth_service::{self, BankAccountCredentials, ExchangeToken, LinkToken}, + ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, + }, + }, +}; + +#[derive(Debug, Clone)] +pub struct Plaid; + +impl ConnectorCommonExt for Plaid +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &auth_types::PaymentAuthRouterData, + _connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + "Content-Type".to_string(), + self.get_content_type().to_string().into(), + )]; + + let mut auth = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut auth); + Ok(header) + } +} + +impl ConnectorCommon for Plaid { + fn id(&self) -> &'static str { + "plaid" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + fn base_url<'a>(&self, _connectors: &'a auth_types::PaymentMethodAuthConnectors) -> &'a str { + "https://sandbox.plaid.com" + } + + fn get_auth_header( + &self, + auth_type: &auth_types::ConnectorAuthType, + ) -> errors::CustomResult)>, errors::ConnectorError> { + let auth = plaid::PlaidAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let client_id = auth.client_id.into_masked(); + let secret = auth.secret.into_masked(); + + Ok(vec![ + ("PLAID-CLIENT-ID".to_string(), client_id), + ("PLAID-SECRET".to_string(), secret), + ]) + } + + fn build_error_response( + &self, + res: auth_types::Response, + ) -> errors::CustomResult { + let response: plaid::PlaidErrorResponse = + res.response + .parse_struct("PlaidErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(auth_types::ErrorResponse { + status_code: res.status_code, + code: crate::consts::NO_ERROR_CODE.to_string(), + message: response.error_message, + reason: response.display_message, + }) + } +} + +impl auth_service::AuthService for Plaid {} +impl auth_service::AuthServiceLinkToken for Plaid {} + +impl ConnectorIntegration + for Plaid +{ + fn get_headers( + &self, + req: &auth_types::LinkTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::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: &auth_types::LinkTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/link/token/create" + )) + } + + fn get_request_body( + &self, + req: &auth_types::LinkTokenRouterData, + ) -> errors::CustomResult { + let req_obj = plaid::PlaidLinkTokenRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &auth_types::LinkTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&auth_types::PaymentAuthLinkTokenType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(auth_types::PaymentAuthLinkTokenType::get_headers( + self, req, connectors, + )?) + .set_body(auth_types::PaymentAuthLinkTokenType::get_request_body( + self, req, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &auth_types::LinkTokenRouterData, + res: auth_types::Response, + ) -> errors::CustomResult { + let response: plaid::PlaidLinkTokenResponse = res + .response + .parse_struct("PlaidLinkTokenResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(auth_types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: auth_types::Response, + ) -> errors::CustomResult { + self.build_error_response(res) + } +} + +impl auth_service::AuthServiceExchangeToken for Plaid {} + +impl + ConnectorIntegration< + ExchangeToken, + auth_types::ExchangeTokenRequest, + auth_types::ExchangeTokenResponse, + > for Plaid +{ + fn get_headers( + &self, + req: &auth_types::ExchangeTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::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: &auth_types::ExchangeTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/item/public_token/exchange" + )) + } + + fn get_request_body( + &self, + req: &auth_types::ExchangeTokenRouterData, + ) -> errors::CustomResult { + let req_obj = plaid::PlaidExchangeTokenRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &auth_types::ExchangeTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&auth_types::PaymentAuthExchangeTokenType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(auth_types::PaymentAuthExchangeTokenType::get_headers( + self, req, connectors, + )?) + .set_body(auth_types::PaymentAuthExchangeTokenType::get_request_body( + self, req, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &auth_types::ExchangeTokenRouterData, + res: auth_types::Response, + ) -> errors::CustomResult { + let response: plaid::PlaidExchangeTokenResponse = res + .response + .parse_struct("PlaidExchangeTokenResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(auth_types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: auth_types::Response, + ) -> errors::CustomResult { + self.build_error_response(res) + } +} + +impl auth_service::AuthServiceBankAccountCredentials for Plaid {} + +impl + ConnectorIntegration< + BankAccountCredentials, + auth_types::BankAccountCredentialsRequest, + auth_types::BankAccountCredentialsResponse, + > for Plaid +{ + fn get_headers( + &self, + req: &auth_types::BankDetailsRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::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: &auth_types::BankDetailsRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "/auth/get")) + } + + fn get_request_body( + &self, + req: &auth_types::BankDetailsRouterData, + ) -> errors::CustomResult { + let req_obj = plaid::PlaidBankAccountCredentialsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &auth_types::BankDetailsRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&auth_types::PaymentAuthBankAccountDetailsType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(auth_types::PaymentAuthBankAccountDetailsType::get_headers( + self, req, connectors, + )?) + .set_body( + auth_types::PaymentAuthBankAccountDetailsType::get_request_body(self, req)?, + ) + .build(), + )) + } + + fn handle_response( + &self, + data: &auth_types::BankDetailsRouterData, + res: auth_types::Response, + ) -> errors::CustomResult { + let response: plaid::PlaidBankAccountCredentialsResponse = res + .response + .parse_struct("PlaidBankAccountCredentialsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(auth_types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: auth_types::Response, + ) -> errors::CustomResult { + self.build_error_response(res) + } +} diff --git a/crates/pm_auth/src/connector/plaid/transformers.rs b/crates/pm_auth/src/connector/plaid/transformers.rs new file mode 100644 index 000000000000..5e1ad67aead0 --- /dev/null +++ b/crates/pm_auth/src/connector/plaid/transformers.rs @@ -0,0 +1,294 @@ +use std::collections::HashMap; + +use common_enums::PaymentMethodType; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{core::errors, types}; + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidLinkTokenRequest { + client_name: String, + country_codes: Vec, + language: String, + products: Vec, + user: User, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] + +pub struct User { + pub client_user_id: String, +} + +impl TryFrom<&types::LinkTokenRouterData> for PlaidLinkTokenRequest { + type Error = error_stack::Report; + fn try_from(item: &types::LinkTokenRouterData) -> Result { + Ok(Self { + client_name: item.request.client_name.clone(), + country_codes: item.request.country_codes.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "country_codes", + }, + )?, + language: item.request.language.clone().unwrap_or("en".to_string()), + products: vec!["auth".to_string()], + user: User { + client_user_id: item.request.user_info.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "country_codes", + }, + )?, + }, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidLinkTokenResponse { + link_token: String, +} + +impl + TryFrom> + for types::PaymentAuthRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::LinkTokenResponse { + link_token: item.response.link_token, + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidExchangeTokenRequest { + public_token: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] + +pub struct PlaidExchangeTokenResponse { + pub access_token: String, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::PaymentAuthRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PlaidExchangeTokenResponse, + T, + types::ExchangeTokenResponse, + >, + ) -> Result { + Ok(Self { + response: Ok(types::ExchangeTokenResponse { + access_token: item.response.access_token, + }), + ..item.data + }) + } +} + +impl TryFrom<&types::ExchangeTokenRouterData> for PlaidExchangeTokenRequest { + type Error = error_stack::Report; + fn try_from(item: &types::ExchangeTokenRouterData) -> Result { + Ok(Self { + public_token: item.request.public_token.clone(), + }) + } +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidBankAccountCredentialsRequest { + access_token: String, + options: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] + +pub struct PlaidBankAccountCredentialsResponse { + pub accounts: Vec, + pub numbers: PlaidBankAccountCredentialsNumbers, + // pub item: PlaidBankAccountCredentialsItem, + pub request_id: String, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct BankAccountCredentialsOptions { + account_ids: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] + +pub struct PlaidBankAccountCredentialsAccounts { + pub account_id: String, + pub name: String, + pub subtype: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsBalances { + pub available: Option, + pub current: Option, + pub limit: Option, + pub iso_currency_code: Option, + pub unofficial_currency_code: Option, + pub last_updated_datetime: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsNumbers { + pub ach: Vec, + pub eft: Vec, + pub international: Vec, + pub bacs: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsItem { + pub item_id: String, + pub institution_id: Option, + pub webhook: Option, + pub error: Option, +} +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsACH { + pub account_id: String, + pub account: String, + pub routing: String, + pub wire_routing: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsEFT { + pub account_id: String, + pub account: String, + pub institution: String, + pub branch: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsInternational { + pub account_id: String, + pub iban: String, + pub bic: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsBacs { + pub account_id: String, + pub account: String, + pub sort_code: String, +} + +impl TryFrom<&types::BankDetailsRouterData> for PlaidBankAccountCredentialsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::BankDetailsRouterData) -> Result { + Ok(Self { + access_token: item.request.access_token.clone(), + options: item.request.optional_ids.as_ref().map(|bank_account_ids| { + BankAccountCredentialsOptions { + account_ids: bank_account_ids.ids.clone(), + } + }), + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + PlaidBankAccountCredentialsResponse, + T, + types::BankAccountCredentialsResponse, + >, + > for types::PaymentAuthRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PlaidBankAccountCredentialsResponse, + T, + types::BankAccountCredentialsResponse, + >, + ) -> Result { + let (account_numbers, accounts_info) = (item.response.numbers, item.response.accounts); + let mut bank_account_vec = Vec::new(); + let mut id_to_suptype = HashMap::new(); + + accounts_info.into_iter().for_each(|acc| { + id_to_suptype.insert(acc.account_id, (acc.subtype, acc.name)); + }); + + account_numbers.ach.into_iter().for_each(|ach| { + let (acc_type, acc_name) = + if let Some((_type, name)) = id_to_suptype.get(&ach.account_id) { + (_type.to_owned(), Some(name.clone())) + } else { + (None, None) + }; + + let bank_details_new = types::BankAccountDetails { + account_name: acc_name, + account_number: ach.account, + routing_number: ach.routing, + payment_method_type: PaymentMethodType::Ach, + account_id: ach.account_id, + account_type: acc_type, + }; + + bank_account_vec.push(bank_details_new); + }); + + Ok(Self { + response: Ok(types::BankAccountCredentialsResponse { + credentials: bank_account_vec, + }), + ..item.data + }) + } +} +pub struct PlaidAuthType { + pub client_id: Secret, + pub secret: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for PlaidAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { client_id, secret } => Ok(Self { + client_id: client_id.to_owned(), + secret: secret.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidErrorResponse { + pub display_message: Option, + pub error_code: Option, + pub error_message: String, + pub error_type: Option, +} diff --git a/crates/pm_auth/src/consts.rs b/crates/pm_auth/src/consts.rs new file mode 100644 index 000000000000..dac3485ec8fc --- /dev/null +++ b/crates/pm_auth/src/consts.rs @@ -0,0 +1,5 @@ +pub const REQUEST_TIME_OUT: u64 = 30; // will timeout after the mentioned limit +pub const REQUEST_TIMEOUT_ERROR_CODE: &str = "TIMEOUT"; // timeout error code +pub const REQUEST_TIMEOUT_ERROR_MESSAGE: &str = "Connector did not respond in specified time"; // error message for timed out request +pub const NO_ERROR_CODE: &str = "No error code"; +pub const NO_ERROR_MESSAGE: &str = "No error message"; diff --git a/crates/pm_auth/src/core.rs b/crates/pm_auth/src/core.rs new file mode 100644 index 000000000000..629e98fbf874 --- /dev/null +++ b/crates/pm_auth/src/core.rs @@ -0,0 +1 @@ +pub mod errors; diff --git a/crates/pm_auth/src/core/errors.rs b/crates/pm_auth/src/core/errors.rs new file mode 100644 index 000000000000..31b178a6276f --- /dev/null +++ b/crates/pm_auth/src/core/errors.rs @@ -0,0 +1,27 @@ +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum ConnectorError { + #[error("Failed to obtain authentication type")] + FailedToObtainAuthType, + #[error("Missing required field: {field_name}")] + MissingRequiredField { field_name: &'static str }, + #[error("Failed to execute a processing step: {0:?}")] + ProcessingStepFailed(Option), + #[error("Failed to deserialize connector response")] + ResponseDeserializationFailed, + #[error("Failed to encode connector request")] + RequestEncodingFailed, +} + +pub type CustomResult = error_stack::Result; + +#[derive(Debug, thiserror::Error)] +pub enum ParsingError { + #[error("Failed to parse enum: {0}")] + EnumParseFailure(&'static str), + #[error("Failed to parse struct: {0}")] + StructParseFailure(&'static str), + #[error("Failed to serialize to {0} format")] + EncodeError(&'static str), + #[error("Unknown error while parsing")] + UnknownError, +} diff --git a/crates/pm_auth/src/lib.rs b/crates/pm_auth/src/lib.rs new file mode 100644 index 000000000000..60d0e06a1e00 --- /dev/null +++ b/crates/pm_auth/src/lib.rs @@ -0,0 +1,4 @@ +pub mod connector; +pub mod consts; +pub mod core; +pub mod types; diff --git a/crates/pm_auth/src/types.rs b/crates/pm_auth/src/types.rs new file mode 100644 index 000000000000..6f5875247f1f --- /dev/null +++ b/crates/pm_auth/src/types.rs @@ -0,0 +1,152 @@ +pub mod api; + +use std::marker::PhantomData; + +use api::auth_service::{BankAccountCredentials, ExchangeToken, LinkToken}; +use common_enums::PaymentMethodType; +use masking::Secret; +#[derive(Debug, Clone)] +pub struct PaymentAuthRouterData { + pub flow: PhantomData, + pub merchant_id: Option, + pub connector: Option, + pub request: Request, + pub response: Result, + pub connector_auth_type: ConnectorAuthType, + pub connector_http_status_code: Option, +} + +#[derive(Debug, Clone)] +pub struct LinkTokenRequest { + pub client_name: String, + pub country_codes: Option>, + pub language: Option, + pub user_info: Option, +} + +#[derive(Debug, Clone)] +pub struct LinkTokenResponse { + pub link_token: String, +} + +pub type LinkTokenRouterData = + PaymentAuthRouterData; + +#[derive(Debug, Clone)] +pub struct ExchangeTokenRequest { + pub public_token: String, +} + +#[derive(Debug, Clone)] +pub struct ExchangeTokenResponse { + pub access_token: String, +} + +impl From for api_models::pm_auth::ExchangeTokenCreateResponse { + fn from(value: ExchangeTokenResponse) -> Self { + Self { + access_token: value.access_token, + } + } +} + +pub type ExchangeTokenRouterData = + PaymentAuthRouterData; + +#[derive(Debug, Clone)] +pub struct BankAccountCredentialsRequest { + pub access_token: String, + pub optional_ids: Option, +} + +#[derive(Debug, Clone)] +pub struct BankAccountOptionalIDs { + pub ids: Vec, +} + +#[derive(Debug, Clone)] +pub struct BankAccountCredentialsResponse { + pub credentials: Vec, +} + +#[derive(Debug, Clone)] +pub struct BankAccountDetails { + pub account_name: Option, + pub account_number: String, + pub routing_number: String, + pub payment_method_type: PaymentMethodType, + pub account_id: String, + pub account_type: Option, +} + +pub type BankDetailsRouterData = PaymentAuthRouterData< + BankAccountCredentials, + BankAccountCredentialsRequest, + BankAccountCredentialsResponse, +>; + +pub type PaymentAuthLinkTokenType = + dyn self::api::ConnectorIntegration; + +pub type PaymentAuthExchangeTokenType = + dyn self::api::ConnectorIntegration; + +pub type PaymentAuthBankAccountDetailsType = dyn self::api::ConnectorIntegration< + BankAccountCredentials, + BankAccountCredentialsRequest, + BankAccountCredentialsResponse, +>; + +#[derive(Clone, Debug, strum::EnumString, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum PaymentMethodAuthConnectors { + Plaid, +} + +#[derive(Debug, Clone)] +pub struct ResponseRouterData { + pub response: R, + pub data: PaymentAuthRouterData, + pub http_code: u16, +} + +#[derive(Clone, Debug, serde::Serialize)] +pub struct ErrorResponse { + pub code: String, + pub message: String, + pub reason: Option, + pub status_code: u16, +} + +impl ErrorResponse { + fn get_not_implemented() -> Self { + Self { + code: "IR_00".to_string(), + message: "This API is under development and will be made available soon.".to_string(), + reason: None, + status_code: http::StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + } + } +} + +#[derive(Default, Debug, Clone, serde::Deserialize)] +pub enum ConnectorAuthType { + BodyKey { + client_id: Secret, + secret: Secret, + }, + #[default] + NoKey, +} + +#[derive(Clone, Debug)] +pub struct Response { + pub headers: Option, + pub response: bytes::Bytes, + pub status_code: u16, +} + +#[derive(serde::Deserialize, Clone)] +pub struct AuthServiceQueryParam { + pub client_secret: Option, +} diff --git a/crates/pm_auth/src/types/api.rs b/crates/pm_auth/src/types/api.rs new file mode 100644 index 000000000000..3684e34ec052 --- /dev/null +++ b/crates/pm_auth/src/types/api.rs @@ -0,0 +1,167 @@ +pub mod auth_service; + +use std::fmt::Debug; + +use common_utils::{ + errors::CustomResult, + request::{Request, RequestContent}, +}; +use masking::Maskable; + +use crate::{ + core::errors::ConnectorError, + types::{self as auth_types, api::auth_service::AuthService}, +}; + +#[async_trait::async_trait] +pub trait ConnectorIntegration: ConnectorIntegrationAny + Sync { + fn get_headers( + &self, + _req: &super::PaymentAuthRouterData, + _connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> CustomResult)>, ConnectorError> { + Ok(vec![]) + } + + fn get_content_type(&self) -> &'static str { + mime::APPLICATION_JSON.essence_str() + } + + fn get_url( + &self, + _req: &super::PaymentAuthRouterData, + _connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> CustomResult { + Ok(String::new()) + } + + fn get_request_body( + &self, + _req: &super::PaymentAuthRouterData, + ) -> CustomResult { + Ok(RequestContent::Json(Box::new(serde_json::json!(r#"{}"#)))) + } + + fn build_request( + &self, + _req: &super::PaymentAuthRouterData, + _connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> CustomResult, ConnectorError> { + Ok(None) + } + + fn handle_response( + &self, + data: &super::PaymentAuthRouterData, + _res: auth_types::Response, + ) -> CustomResult, ConnectorError> + where + T: Clone, + Req: Clone, + Resp: Clone, + { + Ok(data.clone()) + } + + fn get_error_response( + &self, + _res: auth_types::Response, + ) -> CustomResult { + Ok(auth_types::ErrorResponse::get_not_implemented()) + } + + fn get_5xx_error_response( + &self, + res: auth_types::Response, + ) -> CustomResult { + let error_message = match res.status_code { + 500 => "internal_server_error", + 501 => "not_implemented", + 502 => "bad_gateway", + 503 => "service_unavailable", + 504 => "gateway_timeout", + 505 => "http_version_not_supported", + 506 => "variant_also_negotiates", + 507 => "insufficient_storage", + 508 => "loop_detected", + 510 => "not_extended", + 511 => "network_authentication_required", + _ => "unknown_error", + }; + Ok(auth_types::ErrorResponse { + code: res.status_code.to_string(), + message: error_message.to_string(), + reason: String::from_utf8(res.response.to_vec()).ok(), + status_code: res.status_code, + }) + } +} + +pub trait ConnectorCommonExt: + ConnectorCommon + ConnectorIntegration +{ + fn build_headers( + &self, + _req: &auth_types::PaymentAuthRouterData, + _connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> CustomResult)>, ConnectorError> { + Ok(Vec::new()) + } +} + +pub type BoxedConnectorIntegration<'a, T, Req, Resp> = + Box<&'a (dyn ConnectorIntegration + Send + Sync)>; + +pub trait ConnectorIntegrationAny: Send + Sync + 'static { + fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp>; +} + +impl ConnectorIntegrationAny for S +where + S: ConnectorIntegration, +{ + fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp> { + Box::new(self) + } +} + +pub trait AuthServiceConnector: AuthService + Send + Debug {} + +impl AuthServiceConnector for T {} + +pub type BoxedPaymentAuthConnector = Box<&'static (dyn AuthServiceConnector + Sync)>; + +#[derive(Clone, Debug)] +pub struct PaymentAuthConnectorData { + pub connector: BoxedPaymentAuthConnector, + pub connector_name: super::PaymentMethodAuthConnectors, +} + +pub trait ConnectorCommon { + fn id(&self) -> &'static str; + + fn get_auth_header( + &self, + _auth_type: &auth_types::ConnectorAuthType, + ) -> CustomResult)>, ConnectorError> { + Ok(Vec::new()) + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a auth_types::PaymentMethodAuthConnectors) -> &'a str; + + fn build_error_response( + &self, + res: auth_types::Response, + ) -> CustomResult { + Ok(auth_types::ErrorResponse { + status_code: res.status_code, + code: crate::consts::NO_ERROR_CODE.to_string(), + message: crate::consts::NO_ERROR_MESSAGE.to_string(), + reason: None, + }) + } +} diff --git a/crates/pm_auth/src/types/api/auth_service.rs b/crates/pm_auth/src/types/api/auth_service.rs new file mode 100644 index 000000000000..35d44970d518 --- /dev/null +++ b/crates/pm_auth/src/types/api/auth_service.rs @@ -0,0 +1,40 @@ +use crate::types::{ + BankAccountCredentialsRequest, BankAccountCredentialsResponse, ExchangeTokenRequest, + ExchangeTokenResponse, LinkTokenRequest, LinkTokenResponse, +}; + +pub trait AuthService: + super::ConnectorCommon + + AuthServiceLinkToken + + AuthServiceExchangeToken + + AuthServiceBankAccountCredentials +{ +} + +#[derive(Debug, Clone)] +pub struct LinkToken; + +pub trait AuthServiceLinkToken: + super::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct ExchangeToken; + +pub trait AuthServiceExchangeToken: + super::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct BankAccountCredentials; + +pub trait AuthServiceBankAccountCredentials: + super::ConnectorIntegration< + BankAccountCredentials, + BankAccountCredentialsRequest, + BankAccountCredentialsResponse, +> +{ +} diff --git a/crates/redis_interface/Cargo.toml b/crates/redis_interface/Cargo.toml index 9d3ae724d432..9d2c6042731b 100644 --- a/crates/redis_interface/Cargo.toml +++ b/crates/redis_interface/Cargo.toml @@ -9,15 +9,16 @@ license.workspace = true [dependencies] error-stack = "0.3.1" -fred = { version = "6.3.0", features = ["metrics", "partial-tracing", "subscriber-client"] } +fred = { version = "7.0.0", features = ["metrics", "partial-tracing", "subscriber-client"] } futures = "0.3" -serde = { version = "1.0.163", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.40" -tokio = "1.28.2" +tokio = "1.35.1" +tokio-stream = {version = "0.1.14", features = ["sync"]} # First party crates common_utils = { version = "0.1.0", path = "../common_utils", features = ["async_ext"] } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } [dev-dependencies] -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index ca85d19d38b0..ce2b138d9237 100644 --- a/crates/redis_interface/src/commands.rs +++ b/crates/redis_interface/src/commands.rs @@ -383,6 +383,7 @@ impl super::RedisConnectionPool { ) -> CustomResult, errors::RedisError> { Ok(self .pool + .next() .hscan::<&str, &str>(key, pattern, count) .filter_map(|value| async move { match value { @@ -562,7 +563,7 @@ impl super::RedisConnectionPool { .await .into_report() .map_err(|err| match err.current_context().kind() { - RedisErrorKind::NotFound => { + RedisErrorKind::NotFound | RedisErrorKind::Parse => { err.change_context(errors::RedisError::StreamEmptyOrNotAvailable) } _ => err.change_context(errors::RedisError::StreamReadFailed), 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/redis_interface/src/lib.rs b/crates/redis_interface/src/lib.rs index bdc79560118d..33d40ebe155d 100644 --- a/crates/redis_interface/src/lib.rs +++ b/crates/redis_interface/src/lib.rs @@ -24,14 +24,14 @@ use std::sync::{atomic, Arc}; use common_utils::errors::CustomResult; use error_stack::{IntoReport, ResultExt}; -use fred::interfaces::ClientLike; pub use fred::interfaces::PubsubInterface; +use fred::{interfaces::ClientLike, prelude::EventInterface}; use router_env::logger; -pub use self::{commands::*, types::*}; +pub use self::types::*; pub struct RedisConnectionPool { - pub pool: fred::pool::RedisPool, + pub pool: fred::prelude::RedisPool, config: RedisConfig, pub subscriber: SubscriberClient, pub publisher: RedisClient, @@ -53,8 +53,10 @@ impl RedisClient { pub async fn new( config: fred::types::RedisConfig, reconnect_policy: fred::types::ReconnectPolicy, + perf: fred::types::PerformanceConfig, ) -> CustomResult { - let client = fred::prelude::RedisClient::new(config, None, Some(reconnect_policy)); + let client = + fred::prelude::RedisClient::new(config, Some(perf), None, Some(reconnect_policy)); client.connect(); client .wait_for_connect() @@ -73,8 +75,10 @@ impl SubscriberClient { pub async fn new( config: fred::types::RedisConfig, reconnect_policy: fred::types::ReconnectPolicy, + perf: fred::types::PerformanceConfig, ) -> CustomResult { - let client = fred::clients::SubscriberClient::new(config, None, Some(reconnect_policy)); + let client = + fred::clients::SubscriberClient::new(config, Some(perf), None, Some(reconnect_policy)); client.connect(); client .wait_for_connect() @@ -117,6 +121,17 @@ impl RedisConnectionPool { .into_report() .change_context(errors::RedisError::RedisConnectionError)?; + let perf = fred::types::PerformanceConfig { + auto_pipeline: conf.auto_pipeline, + default_command_timeout: std::time::Duration::from_secs(conf.default_command_timeout), + max_feed_count: conf.max_feed_count, + backpressure: fred::types::BackpressureConfig { + disable_auto_backpressure: conf.disable_auto_backpressure, + max_in_flight_commands: conf.max_in_flight_commands, + policy: fred::types::BackpressurePolicy::Drain, + }, + }; + if !conf.use_legacy_version { config.version = fred::types::RespVersion::RESP3; } @@ -127,13 +142,21 @@ impl RedisConnectionPool { conf.reconnect_delay, ); - let subscriber = SubscriberClient::new(config.clone(), reconnect_policy.clone()).await?; + let subscriber = + SubscriberClient::new(config.clone(), reconnect_policy.clone(), perf.clone()).await?; - let publisher = RedisClient::new(config.clone(), reconnect_policy.clone()).await?; + let publisher = + RedisClient::new(config.clone(), reconnect_policy.clone(), perf.clone()).await?; - let pool = fred::pool::RedisPool::new(config, None, Some(reconnect_policy), conf.pool_size) - .into_report() - .change_context(errors::RedisError::RedisConnectionError)?; + let pool = fred::prelude::RedisPool::new( + config, + Some(perf), + None, + Some(reconnect_policy), + conf.pool_size, + ) + .into_report() + .change_context(errors::RedisError::RedisConnectionError)?; pool.connect(); pool.wait_for_connect() @@ -153,16 +176,28 @@ impl RedisConnectionPool { } pub async fn on_error(&self, tx: tokio::sync::oneshot::Sender<()>) { - while let Ok(redis_error) = self.pool.on_error().recv().await { - logger::error!(?redis_error, "Redis protocol or connection error"); - logger::error!("current state: {:#?}", self.pool.state()); - if self.pool.state() == fred::types::ClientState::Disconnected { - if tx.send(()).is_err() { - logger::error!("The redis shutdown signal sender failed to signal"); + use futures::StreamExt; + use tokio_stream::wrappers::BroadcastStream; + + let error_rxs: Vec> = self + .pool + .clients() + .iter() + .map(|client| BroadcastStream::new(client.error_rx())) + .collect(); + + let mut error_rx = futures::stream::select_all(error_rxs); + loop { + if let Some(Ok(error)) = error_rx.next().await { + logger::error!(?error, "Redis protocol or connection error"); + if self.pool.state() == fred::types::ClientState::Disconnected { + if tx.send(()).is_err() { + logger::error!("The redis shutdown signal sender failed to signal"); + } + self.is_redis_available + .store(false, atomic::Ordering::SeqCst); + break; } - self.is_redis_available - .store(false, atomic::Ordering::SeqCst); - break; } } } diff --git a/crates/redis_interface/src/types.rs b/crates/redis_interface/src/types.rs index 5dc93070014e..364fcfabc15a 100644 --- a/crates/redis_interface/src/types.rs +++ b/crates/redis_interface/src/types.rs @@ -52,6 +52,11 @@ pub struct RedisSettings { /// TTL for hash-tables in seconds pub default_hash_ttl: u32, pub stream_read_count: u64, + pub auto_pipeline: bool, + pub disable_auto_backpressure: bool, + pub max_in_flight_commands: u64, + pub default_command_timeout: u64, + pub max_feed_count: u64, } impl RedisSettings { @@ -89,6 +94,11 @@ impl Default for RedisSettings { default_ttl: 300, stream_read_count: 1, default_hash_ttl: 900, + auto_pipeline: true, + disable_auto_backpressure: false, + max_in_flight_commands: 5000, + default_command_timeout: 0, + max_feed_count: 200, } } } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 01595dc18cd5..f60b0c25e934 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,18 +9,19 @@ readme = "README.md" license.workspace = true [features] -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"] -basilisk = ["kms"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] +aws_s3 = ["external_services/aws_s3"] +kms = ["external_services/kms"] +hashicorp-vault = ["external_services/hashicorp-vault"] +email = ["external_services/email", "olap"] +frm = [] 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"] +release = ["kms", "stripe", "aws_s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen", "recon"] +olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap","api_models/olap","dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] accounts_cache = [] -openapi = ["olap", "oltp", "payouts"] +openapi = ["olap", "oltp", "payouts", "dep:openapi"] vergen = ["router_env/vergen"] backwards_compatibility = ["api_models/backwards_compatibility"] business_profile_routing = ["api_models/business_profile_routing"] @@ -30,6 +31,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] payouts = [] +recon = ["email", "api_models/recon"] retry = [] [dependencies] @@ -40,8 +42,6 @@ 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-trait = "0.1.68" -aws-config = { version = "0.55.3", optional = true } -aws-sdk-s3 = { version = "0.28.0", optional = true } base64 = "0.21.2" bb8 = "0.8" bigdecimal = "0.3.1" @@ -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,24 +76,24 @@ 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" -serde_path_to_error = "0.1.11" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" +serde_path_to_error = "0.1.14" serde_qs = { version = "0.12.0", optional = true } serde_urlencoded = "0.7.1" -serde_with = "3.0.0" +serde_with = "3.4.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"] } -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } unicode-segmentation = "1.10.1" url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } -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" @@ -101,12 +101,15 @@ 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"] } +pm_auth = { version = "0.1.0", path = "../pm_auth", package = "pm_auth" } external_services = { version = "0.1.0", path = "../external_services" } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } masking = { version = "0.1.0", path = "../masking" } @@ -115,6 +118,11 @@ 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 } +openapi = { version = "0.1.0", path = "../openapi", optional = true } +erased-serde = "0.3.31" +quick-xml = { version = "0.31.0", features = ["serialize"] } +rdkafka = "0.36.0" +actix-http = "3.3.1" [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } @@ -126,8 +134,9 @@ derive_deref = "1.1.1" rand = "0.8.5" serial_test = "2.0.0" time = { version = "0.3.21", features = ["macros"] } -tokio = "1.28.2" -wiremock = "0.5" +tokio = "1.35.1" +wiremock = "0.5.18" + # First party dev-dependencies test_utils = { version = "0.1.0", path = "../test_utils" } diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index d57403d92989..325ca980243c 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -1,129 +1,585 @@ -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::*; -impl Default for AnalyticsProvider { - fn default() -> Self { - Self::Sqlx(SqlxClient::default()) - } -} +pub mod routes { + use actix_web::{web, Responder, Scope}; + use analytics::{ + api_event::api_events_core, connector_events::connector_events_core, + errors::AnalyticsError, lambda_utils::invoke_lambda, + outgoing_webhook_event::outgoing_webhook_events_core, 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; -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 - } - } - }, - &metrics::METRIC_FETCH_TIME, - metric, - 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> { - match self { - Self::Sqlx(pool) => { - metric - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, + use crate::{ + core::api_locking, + db::user::UserInterface, + routes::AppState, + services::{ + api, + authentication::{self as auth, 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("connector_event_logs") + .route(web::get().to(get_connector_events)), + ) + .service( + web::resource("outgoing_webhook_event_logs") + .route(web::get().to(get_outgoing_webhook_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)), ) - .await } + route } } - 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_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 + } + + /// # 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; + 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, - ), - } + .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 }, -} + /// # 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 + } -impl Default for AnalyticsConfig { - fn default() -> Self { - Self::Sqlx { - sqlx: Database::default(), - } + /// # 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 get_outgoing_webhook_events( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Query< + api_models::analytics::outgoing_webhook_event::OutgoingWebhookLogsRequest, + >, + ) -> impl Responder { + let flow = AnalyticsFlow::GetOutgoingWebhookEvents; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + outgoing_webhook_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 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 + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn generate_refund_report( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GenerateRefundReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move { + 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 flow = AnalyticsFlow::GenerateDisputeReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move { + 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 flow = AnalyticsFlow::GeneratePaymentReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move { + 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 + } + + pub async fn get_connector_events( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Query, + ) -> impl Responder { + let flow = AnalyticsFlow::GetConnectorEvents; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + connector_events_core(&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 beb2869f998c..094f799cb27b 100644 --- a/crates/router/src/bin/router.rs +++ b/crates/router/src/bin/router.rs @@ -9,24 +9,6 @@ async fn main() -> ApplicationResult<()> { // get commandline config before initializing config let cmd_line = ::parse(); - #[cfg(feature = "openapi")] - { - use router::configs::settings::Subcommand; - if let Some(Subcommand::GenerateOpenapiSpec) = cmd_line.subcommand { - let file_path = "openapi/openapi_spec.json"; - #[allow(clippy::expect_used)] - std::fs::write( - file_path, - ::openapi() - .to_pretty_json() - .expect("Failed to serialize OpenAPI specification as JSON"), - ) - .expect("Failed to write OpenAPI specification to file"); - println!("Successfully saved OpenAPI specification file at '{file_path}'"); - return Ok(()); - } - } - #[allow(clippy::expect_used)] let conf = Settings::with_config_path(cmd_line.config_path) .expect("Unable to construct application configuration"); @@ -34,6 +16,9 @@ async fn main() -> ApplicationResult<()> { conf.validate() .expect("Failed to validate router configuration"); + #[cfg(feature = "vergen")] + println!("Starting router (Version: {})", router_env::git_tag!()); + let _guard = router_env::setup( &conf.log, router_env::service_name!(), diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 4c19408582bc..5f98cd880141 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -1,36 +1,40 @@ #![recursion_limit = "256"] use std::{str::FromStr, sync::Arc}; +use actix_web::{dev::Server, web, Scope}; +use api_models::health_check::SchedulerHealthCheckResponse; use common_utils::ext_traits::{OptionExt, StringExt}; use diesel_models::process_tracker as storage; use error_stack::ResultExt; use router::{ configs::settings::{CmdLineConf, Settings}, - core::errors::{self, CustomResult}, - logger, routes, services, + core::{ + errors::{self, CustomResult}, + health_check::HealthCheckInterface, + }, + logger, routes, + services::{self, api}, types::storage::ProcessTrackerExt, workflows, }; +use router_env::{instrument, tracing}; use scheduler::{ consumer::workflows::ProcessTrackerWorkflow, errors::ProcessTrackerError, workflows::ProcessTrackerWorkflows, SchedulerAppState, }; use serde::{Deserialize, Serialize}; +use storage_impl::errors::ApplicationError; use strum::EnumString; use tokio::sync::{mpsc, oneshot}; const SCHEDULER_FLOW: &str = "SCHEDULER_FLOW"; - #[tokio::main] async fn main() -> CustomResult<(), ProcessTrackerError> { - // console_subscriber::init(); - let cmd_line = ::parse(); #[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(), @@ -60,12 +64,31 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { let scheduler_flow = scheduler::SchedulerFlow::from_str(&scheduler_flow_str) .expect("Unable to parse SchedulerFlow from environment variable"); + #[cfg(feature = "vergen")] + println!( + "Starting {scheduler_flow} (Version: {})", + router_env::git_tag!() + ); + let _guard = router_env::setup( &state.conf.log, &scheduler_flow_str, [router_env::service_name!()], ); + #[allow(clippy::expect_used)] + let web_server = Box::pin(start_web_server( + state.clone(), + scheduler_flow_str.to_string(), + )) + .await + .expect("Failed to create the server"); + + tokio::spawn(async move { + let _ = web_server.await; + logger::error!("The health check probe stopped working!"); + }); + logger::debug!(startup_config=?state.conf); start_scheduler(&state, scheduler_flow, (tx, rx)).await?; @@ -74,6 +97,106 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { Ok(()) } +pub async fn start_web_server( + state: routes::AppState, + service: String, +) -> errors::ApplicationResult { + let server = state + .conf + .scheduler + .as_ref() + .ok_or(ApplicationError::InvalidConfigurationValueError( + "Scheduler server is invalidly configured".into(), + ))? + .server + .clone(); + + let web_server = actix_web::HttpServer::new(move || { + actix_web::App::new().service(Health::server(state.clone(), service.clone())) + }) + .bind((server.host.as_str(), server.port))? + .workers(server.workers) + .run(); + let _ = web_server.handle(); + + Ok(web_server) +} + +pub struct Health; + +impl Health { + pub fn server(state: routes::AppState, service: String) -> Scope { + web::scope("health") + .app_data(web::Data::new(state)) + .app_data(web::Data::new(service)) + .service(web::resource("").route(web::get().to(health))) + .service(web::resource("/ready").route(web::get().to(deep_health_check))) + } +} + +#[instrument(skip_all)] +pub async fn health() -> impl actix_web::Responder { + logger::info!("Scheduler health was called"); + actix_web::HttpResponse::Ok().body("Scheduler health is good") +} +#[instrument(skip_all)] +pub async fn deep_health_check( + state: web::Data, + service: web::Data, +) -> impl actix_web::Responder { + let report = deep_health_check_func(state, service).await; + match report { + Ok(response) => services::http_response_json( + serde_json::to_string(&response) + .map_err(|err| { + logger::error!(serialization_error=?err); + }) + .unwrap_or_default(), + ), + Err(err) => api::log_and_return_error_response(err), + } +} +#[instrument(skip_all)] +pub async fn deep_health_check_func( + state: web::Data, + service: web::Data, +) -> errors::RouterResult { + logger::info!("{} deep health check was called", service.into_inner()); + + logger::debug!("Database health check begin"); + + let db_status = state.health_check_db().await.map(|_| true).map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Database", + message: err.to_string() + }) + })?; + + logger::debug!("Database health check end"); + + logger::debug!("Redis health check begin"); + + let redis_status = state + .health_check_redis() + .await + .map(|_| true) + .map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Redis", + message: err.to_string() + }) + })?; + + logger::debug!("Redis health check end"); + + let response = SchedulerHealthCheckResponse { + database: db_status, + redis: redis_status, + }; + + Ok(response) +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, EnumString)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 03b59c55036f..759e968125ff 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -33,7 +33,7 @@ pub enum StripeErrorCode { expected_format: String, }, - #[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_06", message = "Refund amount exceeds the payment amount.")] + #[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_06", message = "The refund amount exceeds the amount captured.")] RefundAmountExceedsPaymentAmount { param: String }, #[error(error_type = StripeErrorType::ApiError, code = "payment_intent_authentication_failure", message = "Payment failed while processing with connector. Retry payment.")] @@ -241,6 +241,8 @@ pub enum StripeErrorCode { LockTimeout, #[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "Merchant connector account is configured with invalid {config}")] InvalidConnectorConfiguration { config: String }, + #[error(error_type = StripeErrorType::HyperswitchError, code = "HE_01", message = "Failed to convert currency to minor unit")] + CurrencyConversionFailed, // [#216]: https://github.com/juspay/hyperswitch/issues/216 // Implement the remaining stripe error codes @@ -466,7 +468,8 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::MandateUpdateFailed | errors::ApiErrorResponse::MandateSerializationFailed | errors::ApiErrorResponse::MandateDeserializationFailed - | errors::ApiErrorResponse::InternalServerError => Self::InternalServerError, // not a stripe code + | errors::ApiErrorResponse::InternalServerError + | errors::ApiErrorResponse::HealthCheckError { .. } => Self::InternalServerError, // not a stripe code errors::ApiErrorResponse::ExternalConnectorError { code, message, @@ -518,6 +521,7 @@ impl From for StripeErrorCode { connector_name, }, errors::ApiErrorResponse::DuplicatePaymentMethod => Self::DuplicatePaymentMethod, + errors::ApiErrorResponse::PaymentBlocked => Self::PaymentFailed, errors::ApiErrorResponse::ClientSecretInvalid => Self::PaymentIntentInvalidParameter { param: "client_secret".to_owned(), }, @@ -595,6 +599,7 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::InvalidConnectorConfiguration { config } => { Self::InvalidConnectorConfiguration { config } } + errors::ApiErrorResponse::CurrencyConversionFailed => Self::CurrencyConversionFailed, } } } @@ -662,7 +667,8 @@ impl actix_web::ResponseError for StripeErrorCode { | Self::CurrencyNotSupported { .. } | Self::DuplicateCustomer | Self::PaymentMethodUnactivated - | Self::InvalidConnectorConfiguration { .. } => StatusCode::BAD_REQUEST, + | Self::InvalidConnectorConfiguration { .. } + | Self::CurrencyConversionFailed => StatusCode::BAD_REQUEST, Self::RefundFailed | Self::PayoutFailed | Self::PaymentLinkNotFound diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index c713011b80c8..aac150b50798 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -118,7 +118,7 @@ impl From for payments::Card { card_number: card.number, card_exp_month: card.exp_month, card_exp_year: card.exp_year, - card_holder_name: card.holder_name.unwrap_or("name".to_string().into()), + card_holder_name: card.holder_name, card_cvc: card.cvc, card_issuer: None, card_network: None, @@ -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, } } @@ -751,6 +753,7 @@ impl ForeignTryFrom<(Option, Option)> for Option, }, QrCodeInformation { - image_data_url: url::Url, + image_data_url: Option, display_to_timestamp: Option, + qr_code_url: Option, }, DisplayVoucherInformation { voucher_details: payments::VoucherNextStepData, @@ -828,9 +832,11 @@ pub(crate) fn into_stripe_next_action( payments::NextActionData::QrCodeInformation { image_data_url, display_to_timestamp, + qr_code_url, } => StripeNextAction::QrCodeInformation { image_data_url, display_to_timestamp, + qr_code_url, }, payments::NextActionData::DisplayVoucherInformation { voucher_details } => { StripeNextAction::DisplayVoucherInformation { voucher_details } diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index dde378e55925..5a2c7a02897e 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -95,7 +95,7 @@ impl From for payments::Card { card_number: card.number, card_exp_month: card.exp_month, card_exp_year: card.exp_year, - card_holder_name: masking::Secret::new("stripe_cust".to_owned()), + card_holder_name: Some(masking::Secret::new("stripe_cust".to_owned())), card_cvc: card.cvc, card_issuer: None, card_network: None, @@ -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 } @@ -382,8 +384,9 @@ pub enum StripeNextAction { session_token: Option, }, QrCodeInformation { - image_data_url: url::Url, + image_data_url: Option, display_to_timestamp: Option, + qr_code_url: Option, }, DisplayVoucherInformation { voucher_details: payments::VoucherNextStepData, @@ -418,9 +421,11 @@ pub(crate) fn into_stripe_next_action( payments::NextActionData::QrCodeInformation { image_data_url, display_to_timestamp, + qr_code_url, } => StripeNextAction::QrCodeInformation { image_data_url, display_to_timestamp, + qr_code_url, }, payments::NextActionData::DisplayVoucherInformation { voucher_details } => { StripeNextAction::DisplayVoucherInformation { voucher_details } diff --git a/crates/router/src/compatibility/stripe/webhooks.rs b/crates/router/src/compatibility/stripe/webhooks.rs index c44e265a9657..807278e0aff2 100644 --- a/crates/router/src/compatibility/stripe/webhooks.rs +++ b/crates/router/src/compatibility/stripe/webhooks.rs @@ -183,6 +183,13 @@ fn get_stripe_event_type(event_type: api_models::enums::EventType) -> &'static s api_models::enums::EventType::DisputeLost => "dispute.lost", api_models::enums::EventType::MandateActive => "mandate.active", api_models::enums::EventType::MandateRevoked => "mandate.revoked", + + // as per this doc https://stripe.com/docs/api/events/types#event_types-payment_intent.amount_capturable_updated + api_models::enums::EventType::PaymentAuthorized => { + "payment_intent.amount_capturable_updated" + } + // stripe treats partially captured payments as succeeded. + api_models::enums::EventType::PaymentCaptured => "payment_intent.succeeded", } } diff --git a/crates/router/src/compatibility/wrap.rs b/crates/router/src/compatibility/wrap.rs index 1ab156d32ad4..d3ca0172f261 100644 --- a/crates/router/src/compatibility/wrap.rs +++ b/crates/router/src/compatibility/wrap.rs @@ -133,19 +133,34 @@ where .map_into_boxed_body() } - Ok(api::ApplicationResponse::PaymenkLinkForm(payment_link_data)) => { - match api::build_payment_link_html(*payment_link_data) { - Ok(rendered_html) => api::http_response_html_data(rendered_html), - Err(_) => api::http_response_err( - r#"{ - "error": { - "message": "Error while rendering payment link html page" - } - }"#, - ), + Ok(api::ApplicationResponse::PaymenkLinkForm(boxed_payment_link_data)) => { + match *boxed_payment_link_data { + api::PaymentLinkAction::PaymentLinkFormData(payment_link_data) => { + match api::build_payment_link_html(payment_link_data) { + Ok(rendered_html) => api::http_response_html_data(rendered_html), + Err(_) => api::http_response_err( + r#"{ + "error": { + "message": "Error while rendering payment link html page" + } + }"#, + ), + } + } + api::PaymentLinkAction::PaymentLinkStatus(payment_link_data) => { + match api::get_payment_link_status(payment_link_data) { + Ok(rendered_html) => api::http_response_html_data(rendered_html), + Err(_) => api::http_response_err( + r#"{ + "error": { + "message": "Error while rendering payment link status page" + } + }"#, + ), + } + } } } - Err(error) => api::log_and_return_error_response(error), }; diff --git a/crates/router/src/configs.rs b/crates/router/src/configs.rs index bb8f61646f26..5cb1df0644ee 100644 --- a/crates/router/src/configs.rs +++ b/crates/router/src/configs.rs @@ -1,4 +1,6 @@ mod defaults; +#[cfg(feature = "hashicorp-vault")] +pub mod hc_vault; #[cfg(feature = "kms")] pub mod kms; pub mod settings; diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index b71e2aad5b5d..7b88c9c2dc79 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, } } } @@ -52,6 +54,8 @@ impl Default for super::settings::Locker { mock_locker: true, basilisk_host: "localhost".into(), locker_signing_key_id: "1".into(), + //true or false + locker_enabled: true, } } } @@ -99,7 +103,7 @@ impl Default for super::settings::DrainerSettings { num_partitions: 64, max_read_count: 100, shutdown_interval: 1000, - loop_interval: 500, + loop_interval: 100, } } } @@ -138,6 +142,19 @@ impl Default for Mandates { connector_list: HashSet::from([ enums::Connector::Stripe, enums::Connector::Adyen, + enums::Connector::Airwallex, + enums::Connector::Authorizedotnet, + enums::Connector::Bankofamerica, + enums::Connector::Bluesnap, + enums::Connector::Checkout, + enums::Connector::Globalpay, + enums::Connector::Multisafepay, + enums::Connector::Noon, + enums::Connector::Nuvei, + enums::Connector::Payu, + enums::Connector::Rapyd, + enums::Connector::Stripe, + enums::Connector::Trustpay, ]), }, ), @@ -501,15 +518,6 @@ impl Default for super::settings::RequiredFields { value: None, } ), - ( - "payment_method_data.card.card_holder_name".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.card.card_holder_name".to_string(), - display_name: "card_holder_name".to_string(), - field_type: enums::FieldType::UserFullName, - value: None, - } - ), ( "email".to_string(), RequiredFieldInfo { @@ -582,7 +590,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, } ), @@ -806,7 +814,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, } ), @@ -1108,6 +1116,89 @@ impl Default for super::settings::RequiredFields { ]), } ), + ( + enums::Connector::Helcim, + 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, + } + ), + ( + "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.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.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::Iatapay, RequiredFieldFinal { @@ -1238,7 +1329,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, } ), @@ -1247,7 +1338,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, } ), @@ -1374,7 +1465,25 @@ impl Default for super::settings::RequiredFields { field_type: enums::FieldType::UserCardCvc, 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, + } + ), ] ), common: HashMap::new(), @@ -1908,14 +2017,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() @@ -2368,7 +2526,7 @@ impl Default for super::settings::RequiredFields { } ), ( - enums::Connector::Bluesnap, + enums::Connector::Bankofamerica, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::from( @@ -2435,81 +2593,63 @@ impl Default for super::settings::RequiredFields { field_type: enums::FieldType::UserBillingName, value: None, } - ) - ] - ), - common: HashMap::new(), - } - ), - ( - enums::Connector::Checkout, - RequiredFieldFinal { - mandate: HashMap::new(), - non_mandate: HashMap::from( - [ + ), ( - "payment_method_data.card.card_number".to_string(), + "billing.address.city".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.card.card_number".to_string(), - display_name: "card_number".to_string(), - field_type: enums::FieldType::UserCardNumber, + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, value: None, } ), ( - "payment_method_data.card.card_exp_month".to_string(), + "billing.address.state".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, + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, value: None, } ), ( - "payment_method_data.card.card_exp_year".to_string(), + "billing.address.zip".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, + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, value: None, } ), ( - "payment_method_data.card.card_cvc".to_string(), + "billing.address.country".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.card.card_cvc".to_string(), - display_name: "card_cvc".to_string(), - field_type: enums::FieldType::UserCardCvc, + 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(), - } - ), - ( - enums::Connector::Coinbase, - RequiredFieldFinal { - mandate: HashMap::new(), - non_mandate: HashMap::from( - [ + ), ( - "billing.address.first_name".to_string(), + "billing.address.line1".to_string(), RequiredFieldInfo { - required_field: "billing.address.first_name".to_string(), - display_name: "billing_first_name".to_string(), - field_type: enums::FieldType::UserBillingName, + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, value: None, } - ) + ), ] ), common: HashMap::new(), } ), ( - enums::Connector::Cybersource, + enums::Connector::Bluesnap, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::from( @@ -2576,16 +2716,157 @@ impl Default for super::settings::RequiredFields { 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, - } - ), + ) + ] + ), + common: HashMap::new(), + } + ), + ( + enums::Connector::Checkout, + 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, + } + ) + ] + ), + common: HashMap::new(), + } + ), + ( + enums::Connector::Coinbase, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "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, + } + ) + ] + ), + common: HashMap::new(), + } + ), + ( + enums::Connector::Cybersource, + 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.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 { @@ -2884,6 +3165,89 @@ impl Default for super::settings::RequiredFields { ]), } ), + ( + enums::Connector::Helcim, + 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, + } + ), + ( + "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.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.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::Iatapay, RequiredFieldFinal { @@ -3014,7 +3378,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, } ), @@ -3023,7 +3387,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, } ), @@ -3150,7 +3514,25 @@ impl Default for super::settings::RequiredFields { field_type: enums::FieldType::UserCardCvc, 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, + } + ), ] ), common: HashMap::new(), @@ -3684,14 +4066,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() @@ -3892,6 +4323,58 @@ impl Default for super::settings::RequiredFields { ( enums::PaymentMethod::BankRedirect, PaymentMethodType(HashMap::from([ + ( + enums::PaymentMethodType::OpenBankingUk, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Volt, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "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, + } + ), + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap:: from([ + ( + "payment_method_data.bank_redirect.open_banking_uk.issuer".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.open_banking_uk.issuer".to_string(), + display_name: "issuer".to_string(), + field_type: enums::FieldType::UserBank, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ) + ]), + }, + ), ( enums::PaymentMethodType::Przelewy24, ConnectorFields { @@ -3911,48 +4394,90 @@ impl Default for super::settings::RequiredFields { ConnectorFields { fields: HashMap::from([ ( - enums::Connector::Stripe, + enums::Connector::Mollie, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::new(), common: HashMap::new(), } ), - ]), - }, - ), - ( - enums::PaymentMethodType::Giropay, - ConnectorFields { - fields: HashMap::from([ ( enums::Connector::Stripe, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::new(), - common: HashMap::new(), + common: HashMap::from([ + ( + "payment_method_data.bank_redirect.bancontact_card.billing_details.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.billing_details.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.bancontact_card.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ) + ]), } ), - ]), - }, - ), - ( - enums::PaymentMethodType::Ideal, - ConnectorFields { - fields: HashMap::from([ ( - enums::Connector::Stripe, + enums::Connector::Adyen, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::new(), - common: HashMap::new(), + common: HashMap::from([ + ( + "payment_method_data.bank_redirect.bancontact_card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.bancontact_card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.bancontact_card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.bancontact_card.card_holder_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.card_holder_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ) + ]), } - ), + ) ]), }, ), ( - enums::PaymentMethodType::Sofort, + enums::PaymentMethodType::Giropay, ConnectorFields { fields: HashMap::from([ ( @@ -3963,28 +4488,44 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), - ]), + ]), }, ), ( - enums::PaymentMethodType::Eps, + enums::PaymentMethodType::Ideal, ConnectorFields { fields: HashMap::from([ ( - enums::Connector::Stripe, + enums::Connector::Aci, RequiredFieldFinal { mandate: HashMap::new(), - non_mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.ideal.bank_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.ideal.bank_name".to_string(), + display_name: "bank_name".to_string(), + field_type: enums::FieldType::UserBank, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.ideal.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.ideal.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "NL".to_string(), + ] + }, + value: None, + } + ) + ]), common: HashMap::new(), } - ) - ]), - }, - ), - ( - enums::PaymentMethodType::Blik, - ConnectorFields { - fields: HashMap::from([ + ), ( enums::Connector::Adyen, RequiredFieldFinal { @@ -3992,11 +4533,11 @@ impl Default for super::settings::RequiredFields { non_mandate: HashMap::new(), common: HashMap::from([ ( - "payment_method_data.bank_redirect.blik.blik_code".to_string(), + "payment_method_data.bank_redirect.ideal.bank_name".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.bank_redirect.blik.blik_code".to_string(), - display_name: "blik_code".to_string(), - field_type: enums::FieldType::UserBlikCode, + required_field: "payment_method_data.bank_redirect.ideal.bank_name".to_string(), + display_name: "bank_name".to_string(), + field_type: enums::FieldType::UserBank, value: None, } ) @@ -4004,29 +4545,43 @@ impl Default for super::settings::RequiredFields { } ), ( - enums::Connector::Stripe, + enums::Connector::Globalpay, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::new(), - common: HashMap::from([ - ( - "payment_method_data.bank_redirect.blik.blik_code".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.bank_redirect.blik.blik_code".to_string(), - display_name: "blik_code".to_string(), - field_type: enums::FieldType::UserBlikCode, - value: None, - } - ) - ]), + common: HashMap::new(), } ), ( - enums::Connector::Trustpay, + enums::Connector::Mollie, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::new(), - common: HashMap::from([ + common: HashMap::new(), + } + ), + ( + enums::Connector::Nexinets, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Nuvei, + 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 { @@ -4046,27 +4601,834 @@ impl Default for super::settings::RequiredFields { } ), ( - "email".to_string(), + "billing.address.country".to_string(), RequiredFieldInfo { - required_field: "email".to_string(), - display_name: "email".to_string(), - field_type: enums::FieldType::UserEmailAddress, + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "NL".to_string(), + ] + }, value: None, } - ), + ) ]), + common: HashMap::new(), } - ) - ]), - }, - ), - ])), - ), - ( - enums::PaymentMethod::Wallet, - PaymentMethodType(HashMap::from([ - ( - enums::PaymentMethodType::ApplePay, + ), + ( + enums::Connector::Shift4, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.ideal.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.ideal.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserCountry{ + options: vec![ + "NL".to_string(), + ] + }, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Paypal, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.ideal.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.ideal.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.ideal.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.ideal.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserCountry{ + options: vec![ + "NL".to_string(), + ] + }, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Stripe, + RequiredFieldFinal { + mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.ideal.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.ideal.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.ideal.billing_details.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.ideal.billing_details.email".to_string(), + display_name: "billing_email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ) + ]), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Trustpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "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.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![ + "NL".to_string(), + ] + }, + value: None, + } + ), + ]), + common: HashMap::new(), + } + ), + ]), + }, + ), + ( + enums::PaymentMethodType::Sofort, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Aci, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ("payment_method_data.bank_redirect.sofort.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "ES".to_string(), + "GB".to_string(), + "SE".to_string(), + "AT".to_string(), + "NL".to_string(), + "DE".to_string(), + "CH".to_string(), + "BE".to_string(), + "FR".to_string(), + "FI".to_string(), + "IT".to_string(), + "PL".to_string(), + ] + }, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Globalpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from([ + ("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![ + "AT".to_string(), + "BE".to_string(), + "DE".to_string(), + "ES".to_string(), + "IT".to_string(), + "NL".to_string(), + ] + }, + value: None, + } + ) + ]), + } + ), + ( + enums::Connector::Mollie, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Nexinets, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Nuvei, + 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.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ES".to_string(), + "GB".to_string(), + "IT".to_string(), + "DE".to_string(), + "FR".to_string(), + "AT".to_string(), + "BE".to_string(), + "NL".to_string(), + "BE".to_string(), + "SK".to_string(), + ] + }, + value: None, + } + )] + ), + common: HashMap::new(), + } + ), + ( + enums::Connector::Paypal, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ("payment_method_data.bank_redirect.sofort.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "ES".to_string(), + "GB".to_string(), + "AT".to_string(), + "NL".to_string(), + "DE".to_string(), + "BE".to_string(), + ] + }, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.sofort.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Shift4, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Stripe, + RequiredFieldFinal { + mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.sofort.billing_details.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.billing_details.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.sofort.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ) + ]), + non_mandate : HashMap::from([ + ("payment_method_data.bank_redirect.sofort.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "ES".to_string(), + "AT".to_string(), + "NL".to_string(), + "DE".to_string(), + "BE".to_string(), + ] + }, + value: None, + } + )]), + common: HashMap::new( + + ), + } + ), + ( + enums::Connector::Trustpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "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.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![ + "ES".to_string(), + "GB".to_string(), + "SE".to_string(), + "AT".to_string(), + "NL".to_string(), + "DE".to_string(), + "CH".to_string(), + "BE".to_string(), + "FR".to_string(), + "FI".to_string(), + "IT".to_string(), + "PL".to_string(), + ] + }, + value: None, + } + ), + ]), + common: HashMap::new(), + } + ), + ]), + }, + ), + ( + enums::PaymentMethodType::Eps, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from([ + ( + "payment_method_data.bank_redirect.eps.bank_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.eps.bank_name".to_string(), + display_name: "bank_name".to_string(), + field_type: enums::FieldType::UserBank, + value: None, + } + ) + ]), + } + ), + ( + enums::Connector::Stripe, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.eps.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.eps.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Aci, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.eps.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.eps.country".to_string(), + display_name: "bank_account_country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "AT".to_string(), + ] + }, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Globalpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from([ + ( + "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![ + "AT".to_string(), + ] + }, + value: None, + } + ) + ]) + } + ), + ( + enums::Connector::Mollie, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate:HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Paypal, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.eps.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.eps.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.eps.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.eps.country".to_string(), + display_name: "bank_account_country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "AT".to_string(), + ] + }, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Trustpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "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.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![ + "AT".to_string(), + ] + }, + value: None, + } + ), + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Shift4, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate:HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Nuvei, + 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.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "AT".to_string(), + ] + }, + value: None, + } + )] + ), + common: HashMap::new(), + } + ), + ]), + }, + ), + ( + enums::PaymentMethodType::Blik, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from([ + ( + "payment_method_data.bank_redirect.blik.blik_code".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.blik.blik_code".to_string(), + display_name: "blik_code".to_string(), + field_type: enums::FieldType::UserBlikCode, + value: None, + } + ) + ]), + } + ), + ( + enums::Connector::Stripe, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from([ + ( + "payment_method_data.bank_redirect.blik.blik_code".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.blik.blik_code".to_string(), + display_name: "blik_code".to_string(), + field_type: enums::FieldType::UserBlikCode, + value: None, + } + ) + ]), + } + ), + ( + enums::Connector::Trustpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: 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.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, + } + ), + ]), + } + ) + ]), + }, + ), + ])), + ), + ( + enums::PaymentMethod::Wallet, + PaymentMethodType(HashMap::from([ + ( + enums::PaymentMethodType::ApplePay, ConnectorFields { fields: HashMap::from([ ( @@ -4076,6 +5438,180 @@ impl Default for super::settings::RequiredFields { non_mandate: HashMap::new(), 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(), + } + ), + ( + enums::Connector::Cybersource, + 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(), + } ) ]), }, @@ -4084,6 +5620,344 @@ impl Default for super::settings::RequiredFields { enums::PaymentMethodType::GooglePay, ConnectorFields { fields: HashMap::from([ + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + 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(), + } + ), + ( + enums::Connector::Bluesnap, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Noon, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Nuvei, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Airwallex, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Authorizedotnet, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Checkout, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Globalpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Multisafepay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "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, + } + ), + ( + "billing.address.line2".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line2".to_string(), + display_name: "line2".to_string(), + field_type: enums::FieldType::UserAddressLine2, + value: None, + } + )]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Cybersource, + 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(), + } + ), + ( + enums::Connector::Payu, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Rapyd, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), ( enums::Connector::Stripe, RequiredFieldFinal { @@ -4092,6 +5966,14 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Trustpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), ]), }, ), @@ -4356,8 +6238,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/hc_vault.rs b/crates/router/src/configs/hc_vault.rs new file mode 100644 index 000000000000..f20d8e79ed89 --- /dev/null +++ b/crates/router/src/configs/hc_vault.rs @@ -0,0 +1,134 @@ +use external_services::hashicorp_vault::{ + decrypt::VaultFetch, Engine, HashiCorpError, HashiCorpVault, +}; +use masking::ExposeInterface; + +use crate::configs::settings; + +#[async_trait::async_trait] +impl VaultFetch for settings::Jwekey { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + ( + self.vault_encryption_key, + self.rust_locker_encryption_key, + self.vault_private_key, + self.tunnel_private_key, + ) = ( + masking::Secret::new(self.vault_encryption_key) + .fetch_inner::(client) + .await? + .expose(), + masking::Secret::new(self.rust_locker_encryption_key) + .fetch_inner::(client) + .await? + .expose(), + masking::Secret::new(self.vault_private_key) + .fetch_inner::(client) + .await? + .expose(), + masking::Secret::new(self.tunnel_private_key) + .fetch_inner::(client) + .await? + .expose(), + ); + Ok(self) + } +} + +#[async_trait::async_trait] +impl VaultFetch for settings::Database { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + Ok(Self { + host: self.host, + port: self.port, + dbname: self.dbname, + username: self.username, + password: self.password.fetch_inner::(client).await?, + pool_size: self.pool_size, + connection_timeout: self.connection_timeout, + queue_strategy: self.queue_strategy, + min_idle: self.min_idle, + max_lifetime: self.max_lifetime, + }) + } +} + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl VaultFetch for settings::PayPalOnboarding { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + self.client_id = self.client_id.fetch_inner::(client).await?; + self.client_secret = self.client_secret.fetch_inner::(client).await?; + self.partner_id = self.partner_id.fetch_inner::(client).await?; + Ok(self) + } +} + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl VaultFetch for settings::ConnectorOnboarding { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + self.paypal = self.paypal.fetch_inner::(client).await?; + Ok(self) + } +} diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index 205169fa291b..4e236a512acf 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -13,19 +13,11 @@ impl KmsDecrypt for settings::Jwekey { kms_client: &KmsClient, ) -> CustomResult { ( - self.locker_encryption_key1, - self.locker_encryption_key2, - 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!( - kms_client.decrypt(self.locker_encryption_key1), - kms_client.decrypt(self.locker_encryption_key2), - 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), @@ -63,7 +55,42 @@ 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, }) } } + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl KmsDecrypt for settings::PayPalOnboarding { + type Output = Self; + + async fn decrypt_inner( + mut self, + kms_client: &KmsClient, + ) -> CustomResult { + self.client_id = kms_client.decrypt(self.client_id.expose()).await?.into(); + self.client_secret = kms_client + .decrypt(self.client_secret.expose()) + .await? + .into(); + self.partner_id = kms_client.decrypt(self.partner_id.expose()).await?.into(); + Ok(self) + } +} + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl KmsDecrypt for settings::ConnectorOnboarding { + type Output = Self; + + async fn decrypt_inner( + mut self, + kms_client: &KmsClient, + ) -> CustomResult { + self.paypal = self.paypal.decrypt_inner(kms_client).await?; + Ok(self) + } +} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 0007e636926c..dd6eaa104cb3 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -1,26 +1,33 @@ use std::{ collections::{HashMap, HashSet}, path::PathBuf, - 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}; #[cfg(feature = "email")] use external_services::email::EmailSettings; +use external_services::file_storage::FileStorageConfig; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; #[cfg(feature = "kms")] 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 serde::Deserialize; +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 +77,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, @@ -82,8 +90,9 @@ pub struct Settings { pub api_keys: ApiKeys, #[cfg(feature = "kms")] pub kms: kms::KmsConfig, - #[cfg(feature = "s3")] - pub file_upload_config: FileUploadConfig, + pub file_storage: FileStorageConfig, + #[cfg(feature = "hashicorp-vault")] + pub hc_vault: hashicorp_vault::HashiCorpVaultConfig, pub tokenization: TokenizationConfig, pub connector_customer: ConnectorCustomer, #[cfg(feature = "dummy_connector")] @@ -94,6 +103,7 @@ pub struct Settings { pub required_fields: RequiredFields, pub delayed_session_response: DelayedSessionConfig, pub webhook_source_verification_call: WebhookSourceVerificationCall, + pub payment_method_auth: PaymentMethodAuth, pub connector_request_reference_id_config: ConnectorRequestReferenceIdConfig, #[cfg(feature = "payouts")] pub payouts: Payouts, @@ -107,6 +117,19 @@ pub struct Settings { pub analytics: AnalyticsConfig, #[cfg(feature = "kv_store")] pub kv_config: KvConfig, + #[cfg(feature = "frm")] + pub frm: Frm, + #[cfg(feature = "olap")] + pub report_download_config: ReportConfig, + pub events: EventsConfig, + #[cfg(feature = "olap")] + pub connector_onboarding: ConnectorOnboarding, +} + +#[cfg(feature = "frm")] +#[derive(Debug, Deserialize, Clone, Default)] +pub struct Frm { + pub enabled: bool, } #[derive(Debug, Deserialize, Clone)] @@ -119,6 +142,43 @@ 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 PaymentMethodAuth { + pub redis_expiry: i64, + pub pm_auth_key: String, +} + +#[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 { @@ -130,7 +190,7 @@ pub struct ApplepayMerchantConfigs { #[derive(Debug, Deserialize, Clone, Default)] pub struct MultipleApiVersionSupportedConnectors { - #[serde(deserialize_with = "connector_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub supported_connectors: HashSet, } @@ -144,42 +204,13 @@ pub struct TempLockerEnableConfig(pub HashMap, #[cfg(feature = "payouts")] - #[serde(deserialize_with = "payout_connector_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub payout_connector_list: HashSet, } -fn connector_deser<'a, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - Ok(value - .trim() - .split(',') - .flat_map(api_models::enums::Connector::from_str) - .collect()) -} - -#[cfg(feature = "payouts")] -fn payout_connector_deser<'a, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - Ok(value - .trim() - .split(',') - .flat_map(api_models::enums::PayoutConnectors::from_str) - .collect()) -} - #[cfg(feature = "dummy_connector")] #[derive(Debug, Deserialize, Clone, Default)] pub struct DummyConnector { @@ -220,21 +251,30 @@ pub struct SupportedPaymentMethodTypesForMandate( #[derive(Debug, Deserialize, Clone)] pub struct SupportedConnectorsForMandate { - #[serde(deserialize_with = "connector_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub connector_list: HashSet, } #[derive(Debug, Deserialize, Clone, Default)] pub struct PaymentMethodTokenFilter { - #[serde(deserialize_with = "pm_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub payment_method: HashSet, pub payment_method_type: Option, pub long_lived_token: bool, + pub apple_pay_pre_decrypt_flow: Option, +} + +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub enum ApplePayPreDecryptFlow { + #[default] + ConnectorTokenization, + NetworkTokenization, } #[derive(Debug, Deserialize, Clone, Default)] pub struct TempLockerEnablePaymentMethodFilter { - #[serde(deserialize_with = "pm_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub payment_method: HashSet, } @@ -246,44 +286,14 @@ pub struct TempLockerEnablePaymentMethodFilter { rename_all = "snake_case" )] pub enum PaymentMethodTypeTokenFilter { - #[serde(deserialize_with = "pm_type_deser")] + #[serde(deserialize_with = "deserialize_hashset")] EnableOnly(HashSet), - #[serde(deserialize_with = "pm_type_deser")] + #[serde(deserialize_with = "deserialize_hashset")] DisableOnly(HashSet), #[default] AllAccepted, } -fn pm_deser<'a, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - value - .trim() - .split(',') - .map(diesel_models::enums::PaymentMethod::from_str) - .collect::>() - .map_err(D::Error::custom) -} - -fn pm_type_deser<'a, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - value - .trim() - .split(',') - .map(diesel_models::enums::PaymentMethodType::from_str) - .collect::>() - .map_err(D::Error::custom) -} - #[derive(Debug, Deserialize, Clone, Default)] pub struct BankRedirectConfig( pub HashMap, @@ -293,7 +303,7 @@ pub struct ConnectorBankNames(pub HashMap); #[derive(Debug, Deserialize, Clone)] pub struct BanksVector { - #[serde(deserialize_with = "bank_vec_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub banks: HashSet, } @@ -315,9 +325,9 @@ pub enum PaymentMethodFilterKey { #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct CurrencyCountryFlowFilter { - #[serde(deserialize_with = "currency_set_deser")] + #[serde(deserialize_with = "deserialize_optional_hashset")] pub currency: Option>, - #[serde(deserialize_with = "string_set_deser")] + #[serde(deserialize_with = "deserialize_optional_hashset")] pub country: Option>, pub not_available_flows: Option, } @@ -346,58 +356,6 @@ pub struct RequiredFieldFinal { pub common: HashMap, } -fn string_set_deser<'a, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'a>, -{ - let value = >::deserialize(deserializer)?; - Ok(value.and_then(|inner| { - let list = inner - .trim() - .split(',') - .flat_map(api_models::enums::CountryAlpha2::from_str) - .collect::>(); - match list.len() { - 0 => None, - _ => Some(list), - } - })) -} - -fn currency_set_deser<'a, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'a>, -{ - let value = >::deserialize(deserializer)?; - Ok(value.and_then(|inner| { - let list = inner - .trim() - .split(',') - .flat_map(api_models::enums::Currency::from_str) - .collect::>(); - match list.len() { - 0 => None, - _ => Some(list), - } - })) -} - -fn bank_vec_deser<'a, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - Ok(value - .trim() - .split(',') - .flat_map(api_models::enums::BankNames::from_str) - .collect()) -} - #[derive(Debug, Default, Deserialize, Clone)] #[serde(default)] pub struct Secrets { @@ -424,6 +382,7 @@ pub struct Locker { pub mock_locker: bool, pub basilisk_host: String, pub locker_signing_key_id: String, + pub locker_enabled: bool, } #[derive(Debug, Deserialize, Clone)] @@ -442,12 +401,6 @@ pub struct EphemeralConfig { #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct Jwekey { - pub locker_key_identifier1: String, - pub locker_key_identifier2: String, - pub locker_encryption_key1: String, - pub locker_encryption_key2: String, - 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, @@ -484,23 +437,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"))] @@ -514,7 +452,9 @@ impl From for storage_impl::config::Database { dbname: val.dbname, pool_size: val.pool_size, connection_timeout: val.connection_timeout, - queue_strategy: val.queue_strategy.into(), + queue_strategy: val.queue_strategy, + min_idle: val.min_idle, + max_lifetime: val.max_lifetime, } } } @@ -570,10 +510,13 @@ pub struct Connectors { pub payme: ConnectorParams, pub paypal: ConnectorParams, pub payu: ConnectorParams, + pub placetopay: ConnectorParams, pub powertranz: ConnectorParams, pub prophetpay: ConnectorParams, pub rapyd: ConnectorParams, + pub riskified: ConnectorParams, pub shift4: ConnectorParams, + pub signifyd: ConnectorParams, pub square: ConnectorParams, pub stax: ConnectorParams, pub stripe: ConnectorParamsWithFileUploadUrl, @@ -666,25 +609,15 @@ pub struct ApiKeys { pub expiry_reminder_days: Vec, } -#[cfg(feature = "s3")] -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(default)] -pub struct FileUploadConfig { - /// The AWS region to send file uploads - pub region: String, - /// The AWS s3 bucket to send file uploads - pub bucket_name: String, -} - #[derive(Debug, Deserialize, Clone, Default)] pub struct DelayedSessionConfig { - #[serde(deserialize_with = "deser_to_get_connectors")] + #[serde(deserialize_with = "deserialize_hashset")] pub connectors_with_delayed_session_response: HashSet, } #[derive(Debug, Deserialize, Clone, Default)] pub struct WebhookSourceVerificationCall { - #[serde(deserialize_with = "connector_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub connectors_with_webhook_source_verification_call: HashSet, } @@ -701,21 +634,6 @@ pub struct ConnectorRequestReferenceIdConfig { pub merchant_ids_send_payment_id_as_connector_request_id: HashSet, } -fn deser_to_get_connectors<'a, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - value - .trim() - .split(',') - .map(api_models::enums::Connector::from_str) - .collect::>() - .map_err(D::Error::custom) -} - impl Settings { pub fn new() -> ApplicationResult { Self::with_config_path(None) @@ -747,6 +665,7 @@ impl Settings { .list_separator(",") .with_list_parse_key("log.telemetry.route_to_trace") .with_list_parse_key("redis.cluster_urls") + .with_list_parse_key("events.kafka.brokers") .with_list_parse_key("connectors.supported.wallets") .with_list_parse_key("connector_request_reference_id_config.merchant_ids_send_payment_id_as_connector_request_id"), @@ -797,28 +716,14 @@ impl Settings { self.kms .validate() .map_err(|error| ApplicationError::InvalidConfigurationValueError(error.into()))?; - #[cfg(feature = "s3")] - self.file_upload_config.validate()?; - self.lock_settings.validate()?; - Ok(()) - } -} -#[cfg(test)] -mod payment_method_deserialization_test { - #![allow(clippy::unwrap_used)] - use serde::de::{ - value::{Error as ValueError, StrDeserializer}, - IntoDeserializer, - }; - - use super::*; + self.file_storage + .validate() + .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?; - #[test] - fn test_pm_deserializer() { - let deserializer: StrDeserializer<'_, ValueError> = "wallet,card".into_deserializer(); - let test_pm = pm_deser(deserializer); - assert!(test_pm.is_ok()) + self.lock_settings.validate()?; + self.events.validate()?; + Ok(()) } } @@ -836,7 +741,7 @@ pub struct LockSettings { } impl<'de> Deserialize<'de> for LockSettings { - fn deserialize>(deserializer: D) -> Result { + fn deserialize>(deserializer: D) -> Result { #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct Inner { @@ -848,11 +753,147 @@ impl<'de> Deserialize<'de> for LockSettings { redis_lock_expiry_seconds, delay_between_retries_in_milliseconds, } = Inner::deserialize(deserializer)?; - let redis_lock_expiry_seconds = redis_lock_expiry_seconds * 1000; + let redis_lock_expiry_milliseconds = redis_lock_expiry_seconds * 1000; Ok(Self { redis_lock_expiry_seconds, delay_between_retries_in_milliseconds, - lock_retries: redis_lock_expiry_seconds / delay_between_retries_in_milliseconds, + lock_retries: redis_lock_expiry_milliseconds / delay_between_retries_in_milliseconds, }) } } + +#[cfg(feature = "olap")] +#[derive(Debug, Deserialize, Clone, Default)] +pub struct ConnectorOnboarding { + pub paypal: PayPalOnboarding, +} + +#[cfg(feature = "olap")] +#[derive(Debug, Deserialize, Clone, Default)] +pub struct PayPalOnboarding { + pub client_id: masking::Secret, + pub client_secret: masking::Secret, + pub partner_id: masking::Secret, + pub enabled: bool, +} + +fn deserialize_hashset_inner(value: impl AsRef) -> Result, String> +where + T: Eq + std::str::FromStr + std::hash::Hash, + ::Err: std::fmt::Display, +{ + let (values, errors) = value + .as_ref() + .trim() + .split(',') + .map(|s| { + T::from_str(s.trim()).map_err(|error| { + format!( + "Unable to deserialize `{}` as `{}`: {error}", + s.trim(), + std::any::type_name::() + ) + }) + }) + .fold( + (HashSet::new(), Vec::new()), + |(mut values, mut errors), result| match result { + Ok(t) => { + values.insert(t); + (values, errors) + } + Err(error) => { + errors.push(error); + (values, errors) + } + }, + ); + if !errors.is_empty() { + Err(format!("Some errors occurred:\n{}", errors.join("\n"))) + } else { + Ok(values) + } +} + +fn deserialize_hashset<'a, D, T>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'a>, + T: Eq + std::str::FromStr + std::hash::Hash, + ::Err: std::fmt::Display, +{ + use serde::de::Error; + + deserialize_hashset_inner(::deserialize(deserializer)?).map_err(D::Error::custom) +} + +fn deserialize_optional_hashset<'a, D, T>(deserializer: D) -> Result>, D::Error> +where + D: serde::Deserializer<'a>, + T: Eq + std::str::FromStr + std::hash::Hash, + ::Err: std::fmt::Display, +{ + use serde::de::Error; + + >::deserialize(deserializer).map(|value| { + value.map_or(Ok(None), |inner: String| { + let list = deserialize_hashset_inner(inner).map_err(D::Error::custom)?; + match list.len() { + 0 => Ok(None), + _ => Ok(Some(list)), + } + }) + })? +} + +#[cfg(test)] +mod hashset_deserialization_test { + #![allow(clippy::unwrap_used)] + use std::collections::HashSet; + + use serde::de::{ + value::{Error as ValueError, StrDeserializer}, + IntoDeserializer, + }; + + use super::deserialize_hashset; + + #[test] + fn test_payment_method_hashset_deserializer() { + use diesel_models::enums::PaymentMethod; + + let deserializer: StrDeserializer<'_, ValueError> = "wallet,card".into_deserializer(); + let payment_methods = deserialize_hashset::<'_, _, PaymentMethod>(deserializer); + let expected_payment_methods = HashSet::from([PaymentMethod::Wallet, PaymentMethod::Card]); + + assert!(payment_methods.is_ok()); + assert_eq!(payment_methods.unwrap(), expected_payment_methods); + } + + #[test] + fn test_payment_method_hashset_deserializer_with_spaces() { + use diesel_models::enums::PaymentMethod; + + let deserializer: StrDeserializer<'_, ValueError> = + "wallet, card, bank_debit".into_deserializer(); + let payment_methods = deserialize_hashset::<'_, _, PaymentMethod>(deserializer); + let expected_payment_methods = HashSet::from([ + PaymentMethod::Wallet, + PaymentMethod::Card, + PaymentMethod::BankDebit, + ]); + + assert!(payment_methods.is_ok()); + assert_eq!(payment_methods.unwrap(), expected_payment_methods); + } + + #[test] + fn test_payment_method_hashset_deserializer_error() { + use diesel_models::enums::PaymentMethod; + + let deserializer: StrDeserializer<'_, ValueError> = + "wallet, card, unknown".into_deserializer(); + let payment_methods = deserialize_hashset::<'_, _, PaymentMethod>(deserializer); + + assert!(payment_methods.is_err()); + } +} diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index 569262d0d210..910ae7543479 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -127,25 +127,6 @@ impl super::settings::DrainerSettings { } } -#[cfg(feature = "s3")] -impl super::settings::FileUploadConfig { - pub fn validate(&self) -> Result<(), ApplicationError> { - use common_utils::fp_utils::when; - - when(self.region.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "s3 region must not be empty".into(), - )) - })?; - - when(self.bucket_name.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "s3 bucket name must not be empty".into(), - )) - }) - } -} - impl super::settings::ApiKeys { pub fn validate(&self) -> Result<(), ApplicationError> { use common_utils::fp_utils::when; diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 3a83fea0d910..de6e250842c7 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -36,10 +36,13 @@ pub mod payeezy; pub mod payme; pub mod paypal; pub mod payu; +pub mod placetopay; pub mod powertranz; pub mod prophetpay; pub mod rapyd; +pub mod riskified; pub mod shift4; +pub mod signifyd; pub mod square; pub mod stax; pub mod stripe; @@ -62,8 +65,9 @@ pub use self::{ globalpay::Globalpay, globepay::Globepay, gocardless::Gocardless, helcim::Helcim, iatapay::Iatapay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, - payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, powertranz::Powertranz, - prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, square::Square, stax::Stax, - stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, + payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, placetopay::Placetopay, + powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, riskified::Riskified, + shift4::Shift4, signifyd::Signifyd, square::Square, stax::Stax, stripe::Stripe, + trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index f6389c802f9e..bbb88209b273 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -2,6 +2,7 @@ mod result_codes; pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as aci; @@ -20,7 +21,7 @@ use crate::{ self, api::{self, ConnectorCommon}, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -79,6 +80,7 @@ impl ConnectorCommon for Aci { .join("; ") }), attempt_status: None, + connector_transaction_id: None, }) } } @@ -135,6 +137,17 @@ impl > for Aci { // Issue: #173 + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("Setup Mandate flow for Aci".to_string()).into()) + } } impl @@ -201,9 +214,6 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body( - self, req, connectors, - )?) .build(), )) } @@ -283,7 +293,7 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { // encode only for for urlencoded things. let connector_router_data = aci::AciRouterData::try_from(( &self.get_currency_unit(), @@ -292,13 +302,8 @@ impl req, ))?; let connector_req = aci::AciPaymentsRequest::try_from(&connector_router_data)?; - let aci_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(aci_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -320,7 +325,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -392,14 +397,9 @@ impl &self, req: &types::PaymentsCancelRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = aci::AciCancelRequest::try_from(req)?; - let aci_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(aci_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( &self, @@ -412,7 +412,7 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body( + .set_body(types::PaymentsVoidType::get_request_body( self, req, connectors, )?) .build(), @@ -488,7 +488,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = aci::AciRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -496,12 +496,7 @@ impl services::ConnectorIntegration::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(body)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -517,7 +512,7 @@ impl services::ConnectorIntegration, - ) -> 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..97cef72b02a7 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -146,10 +146,14 @@ impl ) -> Result { let (item, bank_redirect_data) = value; let payment_data = match bank_redirect_data { - api_models::payments::BankRedirectData::Eps { .. } => { + api_models::payments::BankRedirectData::Eps { country, .. } => { Self::BankRedirect(Box::new(BankRedirectionPMData { payment_brand: PaymentBrand::Eps, - bank_account_country: Some(api_models::enums::CountryAlpha2::AT), + bank_account_country: Some(country.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "eps.country", + }, + )?), bank_account_bank_name: None, bank_account_bic: None, bank_account_iban: None, @@ -174,23 +178,35 @@ impl merchant_transaction_id: None, customer_email: None, })), - api_models::payments::BankRedirectData::Ideal { bank_name, .. } => { - Self::BankRedirect(Box::new(BankRedirectionPMData { - payment_brand: PaymentBrand::Ideal, - bank_account_country: Some(api_models::enums::CountryAlpha2::NL), - bank_account_bank_name: bank_name.to_owned(), - bank_account_bic: None, - bank_account_iban: None, - billing_country: None, - merchant_customer_id: None, - merchant_transaction_id: None, - customer_email: None, - })) - } + api_models::payments::BankRedirectData::Ideal { + bank_name, country, .. + } => Self::BankRedirect(Box::new(BankRedirectionPMData { + payment_brand: PaymentBrand::Ideal, + bank_account_country: Some(country.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "ideal.country", + }, + )?), + bank_account_bank_name: Some(bank_name.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "ideal.bank_name", + }, + )?), + bank_account_bic: None, + bank_account_iban: None, + billing_country: None, + merchant_customer_id: None, + merchant_transaction_id: None, + customer_email: None, + })), api_models::payments::BankRedirectData::Sofort { country, .. } => { Self::BankRedirect(Box::new(BankRedirectionPMData { payment_brand: PaymentBrand::Sofortueberweisung, - bank_account_country: Some(country.to_owned()), + bank_account_country: Some(country.to_owned().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "sofort.country", + }, + )?), bank_account_bank_name: None, bank_account_bic: None, bank_account_iban: None, @@ -254,7 +270,9 @@ impl TryFrom for PaymentDetails { fn try_from(card_data: api_models::payments::Card) -> Result { Ok(Self::AciCard(Box::new(CardDetails { card_number: card_data.card_number, - card_holder: card_data.card_holder_name, + card_holder: card_data + .card_holder_name + .unwrap_or(Secret::new("".to_string())), card_expiry_month: card_data.card_exp_month, card_expiry_year: card_data.card_exp_year, card_cvv: card_data.card_cvc, @@ -409,10 +427,10 @@ impl TryFrom<&AciRouterData<&types::PaymentsAuthorizeRouterData>> for AciPayment | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.router_data.payment_method), - connector: "Aci", - })?, + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Aci"), + ))?, } } } @@ -732,6 +750,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..32f14fd0210f 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -4,21 +4,19 @@ use std::fmt::Debug; use api_models::webhooks::IncomingWebhookEvent; use base64::Engine; +use common_utils::request::RequestContent; use diesel_models::{enums as storage_enums, enums}; use error_stack::{IntoReport, ResultExt}; use ring::hmac; use router_env::{instrument, tracing}; use self::transformers as adyen; +use super::utils as connector_utils; 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}, @@ -30,7 +28,7 @@ use crate::{ domain, transformers::ForeignFrom, }, - utils::{self, crypto, ByteSliceExt, BytesExt, OptionExt}, + utils::{crypto, ByteSliceExt, BytesExt, OptionExt}, }; #[derive(Debug, Clone)] @@ -74,6 +72,7 @@ impl ConnectorCommon for Adyen { message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -173,7 +172,7 @@ impl &self, req: &types::SetupMandateRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let authorize_req = types::PaymentsAuthorizeRouterData::from(( req, types::PaymentsAuthorizeData::from(req), @@ -186,12 +185,7 @@ impl ))?; let connector_req = adyen::AdyenPaymentRequest::try_from(&connector_router_data)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::>::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -204,7 +198,7 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body( + .set_body(types::SetupMandateType::get_request_body( self, req, connectors, )?) .build(), @@ -256,6 +250,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -310,7 +305,7 @@ impl &self, req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -318,12 +313,7 @@ impl req, ))?; let connector_req = adyen::AdyenCaptureRequest::try_from(&connector_router_data)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -338,7 +328,7 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body( + .set_body(types::PaymentsCaptureType::get_request_body( self, req, connectors, )?) .build(), @@ -375,6 +365,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -405,71 +396,48 @@ impl &self, req: &types::RouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - // Adyen doesn't support PSync flow. We use PSync flow to fetch payment details, - // specifically the redirect URL that takes the user to their Payment page. In non-redirection flows, - // we rely on webhooks to obtain the payment status since there is no encoded data available. - // encoded_data only includes the redirect URL and is only relevant in redirection flows. - let encoded_value = req + ) -> CustomResult { + let encoded_data = req .request .encoded_data .clone() - .get_required_value("encoded_data"); - - match encoded_value { - Ok(encoded_data) => { - let adyen_redirection_type = serde_urlencoded::from_str::< - transformers::AdyenRedirectRequestTypes, - >(encoded_data.as_str()) - .into_report() - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - - let redirection_request = match adyen_redirection_type { - adyen::AdyenRedirectRequestTypes::AdyenRedirection(req) => { - adyen::AdyenRedirectRequest { - details: adyen::AdyenRedirectRequestTypes::AdyenRedirection( - adyen::AdyenRedirection { - redirect_result: req.redirect_result, - type_of_redirection_result: None, - result_code: None, - }, - ), - } - } - adyen::AdyenRedirectRequestTypes::AdyenThreeDS(req) => { - adyen::AdyenRedirectRequest { - details: adyen::AdyenRedirectRequestTypes::AdyenThreeDS( - adyen::AdyenThreeDS { - three_ds_result: req.three_ds_result, - type_of_redirection_result: None, - result_code: None, - }, - ), - } - } - adyen::AdyenRedirectRequestTypes::AdyenRefusal(req) => { - adyen::AdyenRedirectRequest { - details: adyen::AdyenRedirectRequestTypes::AdyenRefusal( - adyen::AdyenRefusal { - payload: req.payload, - type_of_redirection_result: None, - result_code: None, - }, - ), - } - } - }; - - let adyen_request = types::RequestBody::log_and_get_request_body( - &redirection_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - Ok(Some(adyen_request)) + .get_required_value("encoded_data") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let adyen_redirection_type = serde_urlencoded::from_str::< + transformers::AdyenRedirectRequestTypes, + >(encoded_data.as_str()) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + let connector_req = match adyen_redirection_type { + adyen::AdyenRedirectRequestTypes::AdyenRedirection(req) => { + adyen::AdyenRedirectRequest { + details: adyen::AdyenRedirectRequestTypes::AdyenRedirection( + adyen::AdyenRedirection { + redirect_result: req.redirect_result, + type_of_redirection_result: None, + result_code: None, + }, + ), + } } - Err(_) => Ok(None), - } + adyen::AdyenRedirectRequestTypes::AdyenThreeDS(req) => adyen::AdyenRedirectRequest { + details: adyen::AdyenRedirectRequestTypes::AdyenThreeDS(adyen::AdyenThreeDS { + three_ds_result: req.three_ds_result, + type_of_redirection_result: None, + result_code: None, + }), + }, + adyen::AdyenRedirectRequestTypes::AdyenRefusal(req) => adyen::AdyenRedirectRequest { + details: adyen::AdyenRedirectRequestTypes::AdyenRefusal(adyen::AdyenRefusal { + payload: req.payload, + type_of_redirection_result: None, + result_code: None, + }), + }, + }; + + Ok(RequestContent::Json(Box::new(connector_req))) } fn get_url( @@ -489,20 +457,30 @@ impl req: &types::RouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let request_body = self.get_request_body(req, connectors)?; - match request_body { - Some(_) => Ok(Some( + // Adyen doesn't support PSync flow. We use PSync flow to fetch payment details, + // specifically the redirect URL that takes the user to their Payment page. In non-redirection flows, + // we rely on webhooks to obtain the payment status since there is no encoded data available. + // encoded_data only includes the redirect URL and is only relevant in redirection flows. + if req + .request + .encoded_data + .clone() + .get_required_value("encoded_data") + .is_ok() + { + Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body( + .set_body(types::PaymentsSyncType::get_request_body( self, req, connectors, )?) .build(), - )), - None => Ok(None), + )) + } else { + Ok(None) } } @@ -546,6 +524,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } @@ -556,7 +535,6 @@ impl } } -#[async_trait::async_trait] impl services::ConnectorIntegration< api::Authorize, @@ -564,49 +542,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, @@ -642,7 +577,7 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -650,12 +585,7 @@ impl req, ))?; let connector_req = adyen::AdyenPaymentRequest::try_from(&connector_router_data)?; - let request_body = types::RequestBody::log_and_get_request_body( - &connector_req, - common_utils::ext_traits::Encode::>::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(request_body)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -663,7 +593,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) @@ -674,7 +603,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -716,32 +645,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 +677,7 @@ impl fn get_url( &self, - _req: &types::PaymentsBalanceRouterData, + _req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult { Ok(format!( @@ -763,33 +688,30 @@ impl fn get_request_body( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = adyen::AdyenBalanceRequest::try_from(req)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::>::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } 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( + .set_body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -798,19 +720,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( @@ -861,15 +811,10 @@ impl &self, req: &types::PaymentsCancelRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = adyen::AdyenCancelRequest::try_from(req)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -882,7 +827,7 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body( + .set_body(types::PaymentsVoidType::get_request_body( self, req, connectors, )?) .build(), @@ -920,6 +865,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -974,14 +920,9 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = adyen::AdyenPayoutCancelRequest::try_from(req)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -994,7 +935,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.destination_currency, @@ -1070,12 +1011,7 @@ impl services::ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1088,7 +1024,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.destination_currency, @@ -1165,12 +1101,7 @@ impl req, ))?; let connector_req = adyen::AdyenPayoutEligibilityRequest::try_from(&connector_router_data)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1187,7 +1118,7 @@ impl .headers(types::PayoutEligibilityType::get_headers( self, req, connectors, )?) - .body(types::PayoutEligibilityType::get_request_body( + .set_body(types::PayoutEligibilityType::get_request_body( self, req, connectors, )?) .build(); @@ -1269,7 +1200,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.destination_currency, @@ -1277,12 +1208,7 @@ impl services::ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1297,7 +1223,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -1379,12 +1305,7 @@ impl services::ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1400,7 +1321,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 +1549,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 +1561,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 ec21c9baa5e9..b141634d46ff 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -9,9 +9,7 @@ use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime, PrimitiveDateTime}; #[cfg(feature = "payouts")] -use crate::connector::utils::AddressDetailsData; -#[cfg(feature = "payouts")] -use crate::types::api::payouts; +use crate::{connector::utils::AddressDetailsData, types::api::payouts, utils::OptionExt}; use crate::{ connector::utils::{ self, BrowserInformationData, CardData, MandateReferenceData, PaymentsAuthorizeRequestData, @@ -213,8 +211,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. @@ -345,6 +343,7 @@ pub struct QrCodeResponseResponse { action: AdyenQrCodeAction, refusal_reason: Option, refusal_reason_code: Option, + additional_data: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -354,17 +353,29 @@ pub struct AdyenQrCodeAction { #[serde(rename = "type")] type_of_response: ActionType, #[serde(rename = "url")] - mobile_redirection_url: Option, + qr_code_url: Option, qr_code_data: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QrCodeAdditionalData { + #[serde(rename = "pix.expirationDate")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pix_expiration_date: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenPtsAction { reference: String, download_url: Option, payment_method_type: PaymentType, - expires_at: Option, + #[serde(rename = "expiresAt")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option_without_timezone" + )] + expires_at: Option, initial_amount: Option, pass_creation_token: Option, total_amount: Option, @@ -397,27 +408,27 @@ 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)] #[serde(tag = "type")] pub enum AdyenPaymentMethod<'a> { - AdyenAffirm(Box), + AdyenAffirm(Box), AdyenCard(Box), - AdyenKlarna(Box), - AdyenPaypal(Box), + AdyenKlarna(Box), + AdyenPaypal(Box), #[serde(rename = "afterpaytouch")] - AfterPay(Box), - AlmaPayLater(Box), - AliPay(Box), - AliPayHk(Box), + AfterPay(Box), + AlmaPayLater(Box), + AliPay(Box), + AliPayHk(Box), ApplePay(Box), #[serde(rename = "atome")] Atome, BancontactCard(Box), - Bizum(Box), + Bizum(Box), Blik(Box), #[serde(rename = "boletobancario")] BoletoBancario, @@ -428,7 +439,7 @@ pub enum AdyenPaymentMethod<'a> { Eps(Box>), #[serde(rename = "gcash")] Gcash(Box), - Giropay(Box), + Giropay(Box), Gpay(Box), #[serde(rename = "gopay_wallet")] GoPay(Box), @@ -437,7 +448,7 @@ pub enum AdyenPaymentMethod<'a> { Kakaopay(Box), Mandate(Box), Mbway(Box), - MobilePay(Box), + MobilePay(Box), #[serde(rename = "momo_wallet")] Momo(Box), #[serde(rename = "momo_atm")] @@ -445,7 +456,7 @@ pub enum AdyenPaymentMethod<'a> { #[serde(rename = "touchngo")] TouchNGo(Box), OnlineBankingCzechRepublic(Box), - OnlineBankingFinland(Box), + OnlineBankingFinland(Box), OnlineBankingPoland(Box), OnlineBankingSlovakia(Box), #[serde(rename = "molpay_ebanking_fpx_MY")] @@ -515,6 +526,13 @@ pub enum AdyenPaymentMethod<'a> { Seicomart(Box), #[serde(rename = "econtext_stores")] PayEasy(Box), + Pix(Box), +} + +#[derive(Debug, Clone, Serialize)] +pub struct PmdForPaymentType { + #[serde(rename = "type")] + payment_type: PaymentType, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -594,11 +612,6 @@ pub struct BancontactCardData { holder_name: Secret, } -#[derive(Debug, Clone, Serialize)] -pub struct MobilePayData { - #[serde(rename = "type")] - payment_type: PaymentType, -} #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct MbwayData { @@ -627,11 +640,6 @@ pub struct PayBrightData { payment_type: PaymentType, } -#[derive(Debug, Clone, Serialize)] -pub struct OnlineBankingFinlandData { - #[serde(rename = "type")] - payment_type: PaymentType, -} #[derive(Debug, Clone, Serialize)] pub struct OnlineBankingCzechRepublicData { #[serde(rename = "type")] @@ -879,7 +887,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", })?, @@ -895,13 +1022,6 @@ pub struct BlikRedirectionData { blik_code: String, } -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BankRedirectionPMData { - #[serde(rename = "type")] - payment_type: PaymentType, -} - #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct BankRedirectionWithIssuer<'a> { @@ -960,23 +1080,6 @@ pub enum CancelStatus { #[default] Processing, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AdyenPaypal { - #[serde(rename = "type")] - payment_type: PaymentType, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AliPayData { - #[serde(rename = "type")] - payment_type: PaymentType, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AliPayHkData { - #[serde(rename = "type")] - payment_type: PaymentType, -} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GoPayData {} @@ -1008,12 +1111,6 @@ pub struct AdyenApplePay { apple_pay_token: Secret, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AdyenPayLaterData { - #[serde(rename = "type")] - payment_type: PaymentType, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DokuBankData { @@ -1153,6 +1250,7 @@ pub enum PaymentType { Seicomart, #[serde(rename = "econtext_stores")] PayEasy, + Pix, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -1380,7 +1478,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 +1490,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 +1608,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(), } } @@ -1587,20 +1686,6 @@ fn get_country_code( address.and_then(|billing| billing.address.as_ref().and_then(|address| address.country)) } -#[cfg(feature = "payouts")] -fn get_payout_card_details(payout_method_data: &PayoutMethodData) -> Option { - match payout_method_data { - PayoutMethodData::Card(card) => Some(PayoutCardDetails { - _type: "scheme".to_string(), // FIXME: Remove hardcoding - number: card.card_number.peek().to_string(), - expiry_month: card.expiry_month.peek().to_string(), - expiry_year: card.expiry_year.peek().to_string(), - holder_name: card.card_holder_name.peek().to_string(), - }), - _ => None, - } -} - fn get_social_security_number( voucher_data: &api_models::payments::VoucherData, ) -> Option> { @@ -1844,19 +1929,19 @@ impl<'a> TryFrom<&api::WalletData> for AdyenPaymentMethod<'a> { Ok(AdyenPaymentMethod::ApplePay(Box::new(apple_pay_data))) } api_models::payments::WalletData::PaypalRedirect(_) => { - let wallet = AdyenPaypal { + let wallet = PmdForPaymentType { payment_type: PaymentType::Paypal, }; Ok(AdyenPaymentMethod::AdyenPaypal(Box::new(wallet))) } api_models::payments::WalletData::AliPayRedirect(_) => { - let alipay_data = AliPayData { + let alipay_data = PmdForPaymentType { payment_type: PaymentType::Alipay, }; Ok(AdyenPaymentMethod::AliPay(Box::new(alipay_data))) } api_models::payments::WalletData::AliPayHkRedirect(_) => { - let alipay_hk_data = AliPayHkData { + let alipay_hk_data = PmdForPaymentType { payment_type: PaymentType::AlipayHk, }; Ok(AdyenPaymentMethod::AliPayHk(Box::new(alipay_hk_data))) @@ -1889,7 +1974,7 @@ impl<'a> TryFrom<&api::WalletData> for AdyenPaymentMethod<'a> { Ok(AdyenPaymentMethod::Mbway(Box::new(mbway_data))) } api_models::payments::WalletData::MobilePayRedirect(_) => { - let data = MobilePayData { + let data = PmdForPaymentType { payment_type: PaymentType::MobilePay, }; Ok(AdyenPaymentMethod::MobilePay(Box::new(data))) @@ -1934,13 +2019,13 @@ impl<'a> TryFrom<(&api::PayLaterData, Option)> let (pay_later_data, country_code) = value; match pay_later_data { api_models::payments::PayLaterData::KlarnaRedirect { .. } => { - let klarna = AdyenPayLaterData { + let klarna = PmdForPaymentType { payment_type: PaymentType::Klarna, }; Ok(AdyenPaymentMethod::AdyenKlarna(Box::new(klarna))) } api_models::payments::PayLaterData::AffirmRedirect { .. } => Ok( - AdyenPaymentMethod::AdyenAffirm(Box::new(AdyenPayLaterData { + AdyenPaymentMethod::AdyenAffirm(Box::new(PmdForPaymentType { payment_type: PaymentType::Affirm, })), ), @@ -1951,7 +2036,7 @@ impl<'a> TryFrom<(&api::PayLaterData, Option)> | api_enums::CountryAlpha2::FR | api_enums::CountryAlpha2::ES | api_enums::CountryAlpha2::GB => Ok(AdyenPaymentMethod::ClearPay), - _ => Ok(AdyenPaymentMethod::AfterPay(Box::new(AdyenPayLaterData { + _ => Ok(AdyenPaymentMethod::AfterPay(Box::new(PmdForPaymentType { payment_type: PaymentType::Afterpaytouch, }))), } @@ -1968,7 +2053,7 @@ impl<'a> TryFrom<(&api::PayLaterData, Option)> Ok(AdyenPaymentMethod::Walley) } api_models::payments::PayLaterData::AlmaRedirect { .. } => Ok( - AdyenPaymentMethod::AlmaPayLater(Box::new(AdyenPayLaterData { + AdyenPaymentMethod::AlmaPayLater(Box::new(PmdForPaymentType { payment_type: PaymentType::Alma, })), ), @@ -2027,7 +2112,7 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod }, ))), api_models::payments::BankRedirectData::Bizum { .. } => { - Ok(AdyenPaymentMethod::Bizum(Box::new(BankRedirectionPMData { + Ok(AdyenPaymentMethod::Bizum(Box::new(PmdForPaymentType { payment_type: PaymentType::Bizum, }))) } @@ -2044,24 +2129,32 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod api_models::payments::BankRedirectData::Eps { bank_name, .. } => Ok( AdyenPaymentMethod::Eps(Box::new(BankRedirectionWithIssuer { payment_type: PaymentType::Eps, - issuer: bank_name - .map(|bank_name| AdyenTestBankNames::try_from(&bank_name)) - .transpose()? - .map(|adyen_bank_name| adyen_bank_name.0), + issuer: Some( + AdyenTestBankNames::try_from(&bank_name.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "eps.bank_name", + }, + )?)? + .0, + ), })), ), - api_models::payments::BankRedirectData::Giropay { .. } => Ok( - AdyenPaymentMethod::Giropay(Box::new(BankRedirectionPMData { + api_models::payments::BankRedirectData::Giropay { .. } => { + Ok(AdyenPaymentMethod::Giropay(Box::new(PmdForPaymentType { payment_type: PaymentType::Giropay, - })), - ), + }))) + } api_models::payments::BankRedirectData::Ideal { bank_name, .. } => Ok( AdyenPaymentMethod::Ideal(Box::new(BankRedirectionWithIssuer { payment_type: PaymentType::Ideal, - issuer: bank_name - .map(|bank_name| AdyenTestBankNames::try_from(&bank_name)) - .transpose()? - .map(|adyen_bank_name| adyen_bank_name.0), + issuer: Some( + AdyenTestBankNames::try_from(&bank_name.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "ideal.bank_name", + }, + )?)? + .0, + ), })), ), api_models::payments::BankRedirectData::OnlineBankingCzechRepublic { issuer } => { @@ -2073,7 +2166,7 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod ))) } api_models::payments::BankRedirectData::OnlineBankingFinland { .. } => Ok( - AdyenPaymentMethod::OnlineBankingFinland(Box::new(OnlineBankingFinlandData { + AdyenPaymentMethod::OnlineBankingFinland(Box::new(PmdForPaymentType { payment_type: PaymentType::OnlineBankingFinland, })), ), @@ -2101,7 +2194,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), @@ -2179,8 +2277,12 @@ impl<'a> TryFrom<&api_models::payments::BankTransferData> for AdyenPaymentMethod last_name: billing_details.last_name.clone(), shopper_email: billing_details.email.clone(), }))), - api_models::payments::BankTransferData::Pix {} - | api_models::payments::BankTransferData::AchBankTransfer { .. } + api_models::payments::BankTransferData::Pix {} => { + Ok(AdyenPaymentMethod::Pix(Box::new(PmdForPaymentType { + payment_type: PaymentType::Pix, + }))) + } + api_models::payments::BankTransferData::AchBankTransfer { .. } | api_models::payments::BankTransferData::SepaBankTransfer { .. } | api_models::payments::BankTransferData::BacsBankTransfer { .. } | api_models::payments::BankTransferData::MultibancoBankTransfer { .. } @@ -2276,7 +2378,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", @@ -2578,7 +2681,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 { @@ -2609,7 +2712,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 { @@ -2617,17 +2720,17 @@ fn get_redirect_extra_details( country, preferred_language, .. - } => ( - Some(preferred_language.to_string()), - Some(country.to_owned()), - ), + } => Ok((preferred_language.clone(), *country)), 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)), } } @@ -2849,18 +2952,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 { @@ -2870,6 +2986,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, @@ -2907,6 +3024,7 @@ pub fn get_adyen_response( reason: response.refusal_reason, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -2930,6 +3048,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)) } @@ -2999,6 +3118,7 @@ pub fn get_redirection_response( reason: None, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -3028,6 +3148,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)) } @@ -3061,6 +3182,7 @@ pub fn get_present_to_shopper_response( reason: None, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -3078,6 +3200,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)) } @@ -3111,6 +3234,7 @@ pub fn get_qr_code_response( reason: None, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -3125,6 +3249,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)) } @@ -3149,6 +3274,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 { @@ -3158,6 +3284,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)) @@ -3169,20 +3296,50 @@ pub fn get_qr_metadata( let image_data = crate_utils::QrImage::new_from_data(response.action.qr_code_data.to_owned()) .change_context(errors::ConnectorError::ResponseHandlingFailed)?; - let image_data_url = Url::parse(image_data.data.as_str()) - .ok() - .ok_or(errors::ConnectorError::ResponseHandlingFailed)?; + let image_data_url = Url::parse(image_data.data.clone().as_str()).ok(); + let qr_code_url = response.action.qr_code_url.clone(); + let display_to_timestamp = response + .additional_data + .clone() + .and_then(|additional_data| additional_data.pix_expiration_date) + .map(|time| utils::get_timestamp_in_milliseconds(&time)); - let qr_code_instructions = payments::QrCodeNextStepsInstruction { - image_data_url, - display_to_timestamp: None, - }; + if let (Some(image_data_url), Some(qr_code_url)) = (image_data_url.clone(), qr_code_url.clone()) + { + let qr_code_info = payments::QrCodeInformation::QrCodeUrl { + image_data_url, + qr_code_url, + display_to_timestamp, + }; + Some(common_utils::ext_traits::Encode::< + payments::QrCodeInformation, + >::encode_to_value(&qr_code_info)) + .transpose() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } else if let (None, Some(qr_code_url)) = (image_data_url.clone(), qr_code_url.clone()) { + let qr_code_info = payments::QrCodeInformation::QrCodeImageUrl { + qr_code_url, + display_to_timestamp, + }; + Some(common_utils::ext_traits::Encode::< + payments::QrCodeInformation, + >::encode_to_value(&qr_code_info)) + .transpose() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } else if let (Some(image_data_url), None) = (image_data_url, qr_code_url) { + let qr_code_info = payments::QrCodeInformation::QrDataUrl { + image_data_url, + display_to_timestamp, + }; - Some(common_utils::ext_traits::Encode::< - payments::QrCodeNextStepsInstruction, - >::encode_to_value(&qr_code_instructions)) - .transpose() - .change_context(errors::ConnectorError::ResponseHandlingFailed) + Some(common_utils::ext_traits::Encode::< + payments::QrCodeInformation, + >::encode_to_value(&qr_code_info)) + .transpose() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } else { + Ok(None) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -3273,7 +3430,8 @@ pub fn get_wait_screen_metadata( | PaymentType::MiniStop | PaymentType::FamilyMart | PaymentType::Seicomart - | PaymentType::PayEasy => Ok(None), + | PaymentType::PayEasy + | PaymentType::Pix => Ok(None), } } @@ -3281,6 +3439,10 @@ pub fn get_present_to_shopper_metadata( response: &PresentToShopperResponse, ) -> errors::CustomResult, errors::ConnectorError> { let reference = response.action.reference.clone(); + let expires_at = response + .action + .expires_at + .map(|time| utils::get_timestamp_in_milliseconds(&time)); match response.action.payment_method_type { PaymentType::Alfamart @@ -3293,7 +3455,7 @@ pub fn get_present_to_shopper_metadata( | PaymentType::Seicomart | PaymentType::PayEasy => { let voucher_data = payments::VoucherNextStepData { - expires_at: response.action.expires_at.clone(), + expires_at, reference, download_url: response.action.download_url.clone(), instructions_url: response.action.instructions_url.clone(), @@ -3317,7 +3479,7 @@ pub fn get_present_to_shopper_metadata( Box::new(payments::DokuBankTransferInstructions { reference: Secret::new(response.action.reference.clone()), instructions_url: response.action.instructions_url.clone(), - expires_at: response.action.expires_at.clone(), + expires_at, }), ); @@ -3376,7 +3538,8 @@ pub fn get_present_to_shopper_metadata( | PaymentType::Vipps | PaymentType::Swish | PaymentType::PaySafeCard - | PaymentType::SevenEleven => Ok(None), + | PaymentType::SevenEleven + | PaymentType::Pix => Ok(None), } } @@ -3450,7 +3613,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(), }, }) @@ -3492,6 +3655,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 @@ -3540,7 +3704,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(), @@ -3622,7 +3786,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)] @@ -3818,12 +3982,12 @@ pub struct AdyenPayoutCreateRequest { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct PayoutBankDetails { - bank_name: String, + iban: Secret, + owner_name: Secret, + bank_city: Option, + bank_name: Option, bic: Option>, - country_code: storage_enums::CountryAlpha2, - iban: Option>, - owner_name: Option>, - bank_city: String, + country_code: Option, tax_id: Option>, } @@ -3874,11 +4038,11 @@ pub struct AdyenPayoutEligibilityRequest { #[serde(rename_all = "camelCase")] pub struct PayoutCardDetails { #[serde(rename = "type")] - _type: String, - number: String, - expiry_month: String, - expiry_year: String, - holder_name: String, + payment_method_type: String, + number: CardNumber, + expiry_month: Secret, + expiry_year: Secret, + holder_name: Secret, } #[cfg(feature = "payouts")] @@ -3933,6 +4097,31 @@ pub struct AdyenPayoutCancelRequest { merchant_account: Secret, } +#[cfg(feature = "payouts")] +impl TryFrom<&PayoutMethodData> for PayoutCardDetails { + type Error = Error; + fn try_from(item: &PayoutMethodData) -> Result { + match item { + PayoutMethodData::Card(card) => Ok(Self { + payment_method_type: "scheme".to_string(), // FIXME: Remove hardcoding + number: card.card_number.clone(), + expiry_month: card.expiry_month.clone(), + expiry_year: card.expiry_year.clone(), + holder_name: card + .card_holder_name + .clone() + .get_required_value("card_holder_name") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "payout_method_data.card.holder_name", + })?, + }), + _ => Err(errors::ConnectorError::MissingRequiredField { + field_name: "payout_method_data.card", + })?, + } + } +} + // Payouts eligibility request transform #[cfg(feature = "payouts")] impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutEligibilityRequest { @@ -3940,15 +4129,10 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutE fn try_from(item: &AdyenRouterData<&types::PayoutsRouterData>) -> Result { let auth_type = AdyenAuthType::try_from(&item.router_data.connector_auth_type)?; let payout_method_data = - get_payout_card_details(&item.router_data.get_payout_method_data()?).map_or( - Err(errors::ConnectorError::MissingRequiredField { - field_name: "payout_method_data", - }), - Ok, - )?; + PayoutCardDetails::try_from(&item.router_data.get_payout_method_data()?)?; 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, @@ -3993,6 +4177,11 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC .customer_details .to_owned() .map_or((None, None), |c| (c.name, c.email)); + let owner_name = owner_name.get_required_value("owner_name").change_context( + errors::ConnectorError::MissingRequiredField { + field_name: "payout_method_data.bank.owner_name", + }, + )?; match item.router_data.get_payout_method_data()? { PayoutMethodData::Card(_) => Err(errors::ConnectorError::NotSupported { @@ -4007,11 +4196,15 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC bank_city: b.bank_city, owner_name, bic: b.bic, - iban: Some(b.iban), + iban: 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", })?, }; @@ -4019,7 +4212,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, @@ -4066,15 +4259,9 @@ 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( - Err(errors::ConnectorError::MissingRequiredField { - field_name: "payout_method_data", - }), - Ok, - )?, + card: PayoutCardDetails::try_from(&item.router_data.get_payout_method_data()?)?, billing_address: get_address_info(item.router_data.get_billing().ok()), merchant_account, reference: item.router_data.request.payout_id.clone(), diff --git a/crates/router/src/connector/airwallex.rs b/crates/router/src/connector/airwallex.rs index 5de7fc065e80..19d69b688c9a 100644 --- a/crates/router/src/connector/airwallex.rs +++ b/crates/router/src/connector/airwallex.rs @@ -2,16 +2,18 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::ext_traits::{ByteSliceExt, ValueExt}; +use common_utils::{ + ext_traits::{ByteSliceExt, ValueExt}, + request::RequestContent, +}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as airwallex; -use super::utils::{AccessTokenRequestInfo, RefundsRequestData}; +use super::utils::{self as connector_utils, AccessTokenRequestInfo, RefundsRequestData}; use crate::{ configs::settings, - connector::utils as connector_utils, core::{ errors::{self, CustomResult}, payments, @@ -27,7 +29,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, RouterData, }, - utils::{self, crypto, BytesExt}, + utils::{crypto, BytesExt}, }; #[derive(Debug, Clone)] @@ -94,6 +96,7 @@ impl ConnectorCommon for Airwallex { message: response.message, reason: response.source, attempt_status: None, + connector_transaction_id: None, }) } } @@ -123,6 +126,16 @@ impl types::PaymentsResponseData, > for Airwallex { + fn build_request( + &self, + _req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Airwallex".to_string()) + .into(), + ) + } } impl api::PaymentToken for Airwallex {} @@ -184,9 +197,6 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = airwallex::AirwallexIntentRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -277,7 +282,7 @@ impl .url(&types::PaymentsInitType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsInitType::get_headers(self, req, connectors)?) - .body(types::PaymentsInitType::get_request_body( + .set_body(types::PaymentsInitType::get_request_body( self, req, connectors, )?) .build(), @@ -379,7 +384,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = airwallex::AirwallexRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -387,12 +392,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(airwallex_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -410,7 +410,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = airwallex::AirwallexCompleteRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( &self, @@ -580,7 +575,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -645,16 +640,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = airwallex::AirwallexPaymentsCaptureRequest::try_from(req)?; - let airwallex_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - Ok(Some(airwallex_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -670,7 +659,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = airwallex::AirwallexPaymentsCancelRequest::try_from(req)?; - let airwallex_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(airwallex_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( &self, @@ -786,7 +770,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = airwallex::AirwallexRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -844,12 +828,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(airwallex_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -864,7 +843,7 @@ impl ConnectorIntegration, - ) -> 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..0686ee6a3b86 100644 --- a/crates/router/src/connector/authorizedotnet.rs +++ b/crates/router/src/connector/authorizedotnet.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::{crypto, ext_traits::ByteSliceExt}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use transformers as authorizedotnet; @@ -21,7 +21,7 @@ use crate::{ self, api::{self, ConnectorCommon, ConnectorCommonExt, PaymentsCompleteAuthorize}, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -118,6 +118,20 @@ impl > for Authorizedotnet { // Issue: #173 + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for Authorizedotnet".to_string(), + ) + .into()) + } } impl ConnectorIntegration @@ -146,7 +160,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -156,12 +170,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(authorizedotnet_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -177,7 +186,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = authorizedotnet::AuthorizedotnetCreateSyncRequest::try_from(req)?; - let sync_request = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(sync_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -265,7 +269,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -342,12 +346,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(authorizedotnet_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -369,7 +368,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = authorizedotnet::CancelOrCaptureTransactionRequest::try_from(req)?; - let authorizedotnet_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(authorizedotnet_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -455,7 +449,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -535,12 +529,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(authorizedotnet_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -555,7 +544,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -632,12 +621,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(sync_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -650,7 +634,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -730,12 +714,7 @@ impl let connector_req = authorizedotnet::PaypalConfirmRequest::try_from(&connector_router_data)?; - let authorizedotnet_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(authorizedotnet_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -753,7 +732,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -875,17 +854,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 +890,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,16 +899,23 @@ fn get_error_response( reason: None, status_code, attempt_status: None, + connector_transaction_id: None, })), Some(authorizedotnet::TransactionResponse::AuthorizedotnetTransactionResponseError(_)) | None => { - let message = &response.messages.message[0].text; + let message = &response + .messages + .message + .first() + .ok_or(errors::ConnectorError::ResponseDeserializationFailed)? + .text; Ok(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), message: message.to_string(), 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..96cf3b6ffc51 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 @@ -617,7 +619,7 @@ impl Some(TransactionResponse::AuthorizedotnetTransactionResponseError(_)) | None => { Ok(Self { status: enums::AttemptStatus::Failure, - response: Err(get_err_response(item.http_code, item.response.messages)), + response: Err(get_err_response(item.http_code, item.response.messages)?), ..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 @@ -685,7 +689,7 @@ impl } None => Ok(Self { status: enums::AttemptStatus::Failure, - response: Err(get_err_response(item.http_code, item.response.messages)), + response: Err(get_err_response(item.http_code, item.response.messages)?), ..item.data }), } @@ -792,6 +796,7 @@ impl TryFrom Ok(Self { - response: Err(get_err_response(item.http_code, item.response.messages)), + response: Err(get_err_response(item.http_code, item.response.messages)?), ..item.data }), } @@ -974,13 +979,14 @@ impl 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 }) } None => Ok(Self { - response: Err(get_err_response(item.http_code, item.response.messages)), + response: Err(get_err_response(item.http_code, item.response.messages)?), ..item.data }), } @@ -1018,14 +1024,22 @@ impl From> for TransactionType { } } -fn get_err_response(status_code: u16, message: ResponseMessages) -> types::ErrorResponse { - types::ErrorResponse { - code: message.message[0].code.clone(), - message: message.message[0].text.clone(), +fn get_err_response( + status_code: u16, + message: ResponseMessages, +) -> Result { + let response_message = message + .message + .first() + .ok_or(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(types::ErrorResponse { + code: response_message.code.clone(), + message: response_message.text.clone(), reason: None, status_code, attempt_status: None, - } + connector_transaction_id: None, + }) } #[derive(Debug, Deserialize)] diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index 802be26408df..37c6ae6cd6ba 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use transformers as bambora; @@ -28,7 +29,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -96,6 +97,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, }) } } @@ -137,6 +139,20 @@ impl types::PaymentsResponseData, > for Bambora { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Bambora".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Bambora {} @@ -174,15 +190,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let request = bambora::BamboraPaymentsRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = bambora::BamboraPaymentsRequest::try_from(req)?; - let bambora_req = types::RequestBody::log_and_get_request_body( - &request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bambora_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -196,7 +207,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = bambora::BamboraPaymentsCaptureRequest::try_from(req)?; - let bambora_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bambora_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -370,7 +376,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let request = bambora::BamboraPaymentsRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = bambora::BamboraPaymentsRequest::try_from(req)?; - let bambora_req = types::RequestBody::log_and_get_request_body( - &request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bambora_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -467,7 +468,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = bambora::BamboraRefundRequest::try_from(req)?; - let bambora_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bambora_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -563,7 +559,7 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } @@ -752,15 +745,10 @@ impl &self, req: &types::PaymentsCompleteAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let request = bambora::BamboraThreedsContinueRequest::try_from(&req.request)?; + ) -> CustomResult { + let connector_req = bambora::BamboraThreedsContinueRequest::try_from(&req.request)?; - let bambora_req = types::RequestBody::log_and_get_request_body( - &request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bambora_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -776,7 +764,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(); diff --git a/crates/router/src/connector/bambora/transformers.rs b/crates/router/src/connector/bambora/transformers.rs index e686186c901b..8f18ca272ddf 100644 --- a/crates/router/src/connector/bambora/transformers.rs +++ b/crates/router/src/connector/bambora/transformers.rs @@ -117,7 +117,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BamboraPaymentsRequest { enums::AuthenticationType::NoThreeDs => None, }; let bambora_card = BamboraCard { - name: req_card.card_holder_name, + name: req_card + .card_holder_name + .unwrap_or(Secret::new("".to_string())), number: req_card.card_number, expiry_month: req_card.card_exp_month, expiry_year: req_card.card_exp_year, @@ -215,6 +217,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 +244,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 51a1d722dc51..589852c6b56f 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -3,6 +3,7 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; @@ -11,6 +12,7 @@ use time::OffsetDateTime; use transformers as bankofamerica; use url::Url; +use super::utils::{PaymentsAuthorizeRequestData, RouterData}; use crate::{ configs::settings, connector::{utils as connector_utils, utils::RefundsRequestData}, @@ -27,7 +29,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; pub const V_C_MERCHANT_ID: &str = "v-c-merchant-id"; @@ -47,6 +49,8 @@ impl api::Refund for Bankofamerica {} impl api::RefundExecute for Bankofamerica {} impl api::RefundSync for Bankofamerica {} impl api::PaymentToken for Bankofamerica {} +impl api::PaymentsPreProcessing for Bankofamerica {} +impl api::PaymentsCompleteAuthorize for Bankofamerica {} impl Bankofamerica { pub fn generate_digest(&self, payload: &[u8]) -> String { @@ -83,7 +87,9 @@ impl Bankofamerica { let key_value = consts::BASE64_ENGINE .decode(api_secret.expose()) .into_report() - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "connector_account_details.api_secret", + })?; 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()); @@ -134,10 +140,8 @@ where .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() - }) + types::RequestBody::get_inner_value(boa_req) + .expose() .as_bytes(), ); let signature = self.generate_signature( @@ -204,36 +208,50 @@ impl ConnectorCommon for Bankofamerica { } 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, - message, - reason: Some(connector_reason), - attempt_status: None, - }) + match response { + transformers::BankOfAmericaErrorResponse::StandardError(response) => { + let (code, connector_reason) = 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 message = match response.details { + Some(details) => details + .iter() + .map(|det| format!("{} : {}", det.field, det.reason)) + .collect::>() + .join(", "), + None => connector_reason.clone(), + }; + + Ok(ErrorResponse { + status_code: res.status_code, + code, + message, + reason: Some(connector_reason), + attempt_status: None, + connector_transaction_id: None, + }) + } + transformers::BankOfAmericaErrorResponse::AuthenticationError(response) => { + Ok(ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: response.response.rmsg.clone(), + reason: Some(response.response.rmsg), + attempt_status: None, + connector_transaction_id: None, + }) + } + } } } @@ -270,6 +288,127 @@ impl types::PaymentsResponseData, > for Bankofamerica { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for Bankofamerica".to_string(), + ) + .into()) + } +} + +impl + ConnectorIntegration< + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > for Bankofamerica +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + 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::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let redirect_response = req.request.redirect_response.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "redirect_response", + }, + )?; + match redirect_response.params { + Some(param) if !param.clone().peek().is_empty() => Ok(format!( + "{}risk/v1/authentications", + self.base_url(connectors) + )), + Some(_) | None => Ok(format!( + "{}risk/v1/authentication-results", + self.base_url(connectors) + )), + } + } + fn get_request_body( + &self, + req: &types::PaymentsPreProcessingRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + 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_req = + bankofamerica::BankOfAmericaPreProcessingRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsPreProcessingType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaPreProcessingResponse = res + .response + .parse_struct("BankOfAmerica AuthEnrollmentResponse") + .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 @@ -289,34 +428,39 @@ impl ConnectorIntegration CustomResult { - Ok(format!( - "{}pts/v2/payments/", - api::ConnectorCommon::base_url(self, connectors) - )) + if req.is_three_ds() && req.request.is_card() { + Ok(format!( + "{}risk/v1/authentication-setups", + self.base_url(connectors) + )) + } else { + Ok(format!("{}pts/v2/payments/", self.base_url(connectors))) + } } fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - 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_payments_request)) + if req.is_three_ds() && req.request.is_card() { + let connector_req = + bankofamerica::BankOfAmericaAuthSetupRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } else { + let connector_req = + bankofamerica::BankOfAmericaPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } } fn build_request( @@ -334,7 +478,7 @@ impl ConnectorIntegration CustomResult { + if data.is_three_ds() && data.request.is_card() { + let response: bankofamerica::BankOfAmericaAuthSetupResponse = res + .response + .parse_struct("Bankofamerica AuthSetupResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } else { + 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) + } + + fn get_5xx_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaServerErrorResponse = res + .response + .parse_struct("BankOfAmericaServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let attempt_status = match response.reason { + Some(reason) => match reason { + transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure), + transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None, + }, + None => None, + }; + Ok(ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status, + connector_transaction_id: None, + }) + } +} + +impl + ConnectorIntegration< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for Bankofamerica +{ + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + 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::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}pts/v2/payments/", self.base_url(connectors))) + } + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = + bankofamerica::BankOfAmericaPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + fn build_request( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCompleteAuthorizeRouterData, + res: Response, + ) -> CustomResult { let response: bankofamerica::BankOfAmericaPaymentsResponse = res .response .parse_struct("BankOfAmerica PaymentResponse") @@ -363,6 +631,33 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaServerErrorResponse = res + .response + .parse_struct("BankOfAmericaServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let attempt_status = match response.reason { + Some(reason) => match reason { + transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure), + transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None, + }, + None => None, + }; + Ok(ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration @@ -470,21 +765,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let connector_request = + let connector_req = 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)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -500,7 +790,7 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaServerErrorResponse = res + .response + .parse_struct("BankOfAmericaServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration @@ -562,7 +873,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request @@ -577,15 +888,10 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bankofamerica_void_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -599,7 +905,7 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaServerErrorResponse = res + .response + .parse_struct("BankOfAmericaServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration @@ -661,20 +988,16 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = bankofamerica::BankOfAmericaRefundRequest::try_from(&connector_router_data)?; - let bankofamerica_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bankofamerica_req)) + let connector_req = + bankofamerica::BankOfAmericaRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -689,7 +1012,7 @@ impl ConnectorIntegration, - ) -> 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 20b2af48b168..0b8158c10af9 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -1,20 +1,26 @@ use api_models::payments; -use common_utils::pii; -use masking::Secret; +use base64::Engine; +use common_utils::{ext_traits::ValueExt, pii}; +use error_stack::{IntoReport, ResultExt}; +use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use crate::{ connector::utils::{ - self, AddressDetailsData, CardData, CardIssuer, PaymentsAuthorizeRequestData, - PaymentsSyncRequestData, RouterData, + self, AddressDetailsData, ApplePayDecrypt, CardData, CardIssuer, + PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData, + PaymentsPreProcessingData, PaymentsSyncRequestData, RouterData, }, consts, core::errors, + services, types::{ self, api::{self, enums as api_enums}, storage::enums, transformers::ForeignFrom, + ApplePayPredecryptData, }, }; @@ -81,12 +87,36 @@ pub struct BankOfAmericaPaymentsRequest { payment_information: PaymentInformation, order_information: OrderInformationWithBill, client_reference_information: ClientReferenceInformation, + #[serde(skip_serializing_if = "Option::is_none")] + consumer_authentication_information: Option, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProcessingInformation { - capture: bool, + capture: Option, + payment_solution: Option, + commerce_indicator: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantDefinedInformation { + key: u8, + value: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthInformation { + ucaf_collection_indicator: Option, + cavv: Option, + ucaf_authentication_data: Option, + xid: Option, + directory_server_transaction_id: Option, + specification_version: Option, } #[derive(Debug, Serialize)] @@ -97,10 +127,45 @@ pub struct CaptureOptions { } #[derive(Debug, Serialize)] -pub struct PaymentInformation { +#[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(rename_all = "camelCase")] +pub struct ApplePayTokenizedCard { + transaction_type: TransactionType, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayTokenPaymentInformation { + fluid_data: FluidData, + tokenized_card: ApplePayTokenizedCard, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayPaymentInformation { + tokenized_card: TokenizedCard, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum PaymentInformation { + Cards(CardPaymentInformation), + GooglePay(GooglePayPaymentInformation), + ApplePay(ApplePayPaymentInformation), + ApplePayToken(ApplePayTokenPaymentInformation), +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Card { @@ -112,6 +177,22 @@ pub struct Card { card_type: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenizedCard { + number: Secret, + expiration_month: Secret, + expiration_year: Secret, + cryptogram: Secret, + transaction_type: TransactionType, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FluidData { + value: Secret, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformationWithBill { @@ -148,12 +229,14 @@ fn build_bill_to( .address .as_ref() .ok_or_else(utils::missing_field_err("billing.address"))?; + let mut state = address.to_state_code()?.peek().clone(); + state.truncate(20); 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()?, + administrative_area: Secret::from(state), postal_code: address.get_zip()?.to_owned(), country: address.get_country()?.to_owned(), email, @@ -177,12 +260,404 @@ impl From for String { } } -#[derive(Debug, Deserialize, Serialize)] +#[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() + } +} + +#[derive(Debug, Serialize)] +pub enum TransactionType { + #[serde(rename = "1")] + ApplePay, +} + +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::PaymentsCompleteAuthorizeRouterData>, + BillTo, + )> for OrderInformationWithBill +{ + fn from( + (item, bill_to): ( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + 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: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + payment_solution: solution.map(String::from), + commerce_indicator: String::from("internet"), + } + } +} + +impl + From<( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + Option, + &BankOfAmericaConsumerAuthValidateResponse, + )> for ProcessingInformation +{ + fn from( + (item, solution, three_ds_data): ( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + Option, + &BankOfAmericaConsumerAuthValidateResponse, + ), + ) -> Self { + Self { + capture: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + payment_solution: solution.map(String::from), + commerce_indicator: three_ds_data + .indicator + .to_owned() + .unwrap_or(String::from("internet")), + } + } +} + +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()), + } + } +} + +impl From<&BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for ClientReferenceInformation +{ + fn from(item: &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>) -> Self { + Self { + code: Some(item.router_data.connector_request_reference_id.clone()), + } + } +} + +impl ForeignFrom for Vec { + fn foreign_from(metadata: Value) -> Self { + let hashmap: std::collections::BTreeMap = + serde_json::from_str(&metadata.to_string()) + .unwrap_or(std::collections::BTreeMap::new()); + let mut vector: Self = Self::new(); + let mut iter = 1; + for (key, value) in hashmap { + vector.push(MerchantDefinedInformation { + key: iter, + value: format!("{key}={value}"), + }); + iter += 1; + } + vector + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { code: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientProcessorInformation { + avs: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientRiskInformation { + rules: Option>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ClientRiskInformationRules { + name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Avs { + code: String, + code_raw: Option, +} + +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + payments::Card, + )> for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, ccard): ( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + payments::Card, + ), + ) -> 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 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 client_reference_information = ClientReferenceInformation::from(item); + + let three_ds_info: BankOfAmericaThreeDSMetadata = item + .router_data + .request + .connector_meta + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "connector_meta", + })? + .parse_value("BankOfAmericaThreeDSMetadata") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + + let processing_information = + ProcessingInformation::from((item, None, &three_ds_info.three_ds_data)); + + let consumer_authentication_information = Some(BankOfAmericaConsumerAuthInformation { + ucaf_collection_indicator: three_ds_info.three_ds_data.ucaf_collection_indicator, + cavv: three_ds_info.three_ds_data.cavv, + ucaf_authentication_data: three_ds_info.three_ds_data.ucaf_authentication_data, + xid: three_ds_info.three_ds_data.xid, + directory_server_transaction_id: three_ds_info + .three_ds_data + .directory_server_transaction_id, + specification_version: three_ds_info.three_ds_data.specification_version, + }); + + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information, + merchant_defined_information, + }) + } +} + +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + )> for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, ccard): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + ), + ) -> 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 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); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + consumer_authentication_information: None, + }) + } +} + +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + Box, + )> for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, apple_pay_data): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + Box, + ), + ) -> 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 processing_information = + ProcessingInformation::from((item, Some(PaymentSolution::ApplePay))); + let client_reference_information = ClientReferenceInformation::from(item); + let expiration_month = apple_pay_data.get_expiry_month()?; + let expiration_year = apple_pay_data.get_four_digit_expiry_year()?; + let payment_information = PaymentInformation::ApplePay(ApplePayPaymentInformation { + tokenized_card: TokenizedCard { + number: apple_pay_data.application_primary_account_number, + cryptogram: apple_pay_data.payment_data.online_payment_cryptogram, + transaction_type: TransactionType::ApplePay, + expiration_year, + expiration_month, + }, + }); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + consumer_authentication_information: None, + }) + } +} + +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); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + consumer_authentication_information: None, + }) + } +} + impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> for BankOfAmericaPaymentsRequest { @@ -191,23 +666,129 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> item: &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(ccard) => { - let email = item.router_data.request.get_email()?; - let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + payments::WalletData::ApplePay(apple_pay_data) => { + match item.router_data.payment_method_token.clone() { + Some(payment_method_token) => match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + Self::try_from((item, decrypt_data)) + } + types::PaymentMethodToken::Token(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }, + None => { + 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 processing_information = ProcessingInformation::from(( + item, + Some(PaymentSolution::ApplePay), + )); + let client_reference_information = + ClientReferenceInformation::from(item); + let payment_information = PaymentInformation::ApplePayToken( + ApplePayTokenPaymentInformation { + fluid_data: FluidData { + value: Secret::from(apple_pay_data.payment_data), + }, + tokenized_card: ApplePayTokenizedCard { + transaction_type: TransactionType::ApplePay, + }, + }, + ); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from( + metadata.peek().to_owned(), + ) + }); + Ok(Self { + processing_information, + payment_information, + order_information, + merchant_defined_information, + client_reference_information, + consumer_authentication_information: None, + }) + } + } + } + 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::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()) + } + } + } +} - let order_information = OrderInformationWithBill { - amount_details: Amount { - total_amount: item.amount.to_owned(), - currency: item.router_data.request.currency, - }, - bill_to, - }; +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaAuthSetupRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, +} + +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> + for BankOfAmericaAuthSetupRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + payments::PaymentMethodData::Card(ccard) => { 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 { + let payment_information = PaymentInformation::Cards(CardPaymentInformation { card: Card { number: ccard.card_number, expiration_month: ccard.card_exp_month, @@ -215,28 +796,500 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> security_code: ccard.card_cvc, card_type, }, - }; + }); + let client_reference_information = ClientReferenceInformation::from(item); + Ok(Self { + payment_information, + client_reference_information, + }) + } + payments::PaymentMethodData::Wallet(_) + | 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("BankOfAmerica"), + ) + .into()) + } + } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum BankofamericaPaymentStatus { + Authorized, + Succeeded, + Failed, + Voided, + Reversed, + Pending, + Declined, + Rejected, + Challenge, + AuthorizedPendingReview, + AuthorizedRiskDeclined, + Transmitted, + InvalidRequest, + ServerError, + PendingAuthentication, + PendingReview, + //PartialAuthorized, not being consumed yet. +} + +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::Charged + } else { + Self::Authorized + } + } + BankofamericaPaymentStatus::Pending => { + if auto_capture { + Self::Charged + } else { + Self::Pending + } + } + BankofamericaPaymentStatus::Succeeded | BankofamericaPaymentStatus::Transmitted => { + Self::Charged + } + BankofamericaPaymentStatus::Voided | BankofamericaPaymentStatus::Reversed => { + Self::Voided + } + BankofamericaPaymentStatus::Failed + | BankofamericaPaymentStatus::Declined + | BankofamericaPaymentStatus::AuthorizedRiskDeclined + | BankofamericaPaymentStatus::InvalidRequest + | BankofamericaPaymentStatus::Rejected + | BankofamericaPaymentStatus::ServerError => Self::Failure, + BankofamericaPaymentStatus::PendingAuthentication => Self::AuthenticationPending, + BankofamericaPaymentStatus::PendingReview | BankofamericaPaymentStatus::Challenge => { + Self::Pending + } + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthInformationResponse { + access_token: String, + device_data_collection_url: String, + reference_id: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientAuthSetupInfoResponse { + id: String, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: BankOfAmericaConsumerAuthInformationResponse, +} - let processing_information = ProcessingInformation { - capture: matches!( - item.router_data.request.capture_method, - Some(enums::CaptureMethod::Automatic) | None +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum BankOfAmericaAuthSetupResponse { + ClientAuthSetupInfo(ClientAuthSetupInfoResponse), + ErrorInformation(BankOfAmericaErrorInformationResponse), +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum BankOfAmericaPaymentsResponse { + ClientReferenceInformation(BankOfAmericaClientReferenceResponse), + ErrorInformation(BankOfAmericaErrorInformationResponse), +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaClientReferenceResponse { + id: String, + status: BankofamericaPaymentStatus, + client_reference_information: ClientReferenceInformation, + processor_information: Option, + risk_information: Option, + error_information: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaErrorInformationResponse { + id: String, + error_information: BankOfAmericaErrorInformation, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct BankOfAmericaErrorInformation { + reason: Option, + message: Option, +} + +impl + From<( + &BankOfAmericaErrorInformationResponse, + types::ResponseRouterData, + Option, + )> for types::RouterData +{ + fn from( + (error_response, item, transaction_status): ( + &BankOfAmericaErrorInformationResponse, + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + T, + types::PaymentsResponseData, + >, + Option, + ), + ) -> Self { + let error_reason = error_response + .error_information + .message + .to_owned() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason.to_owned(); + let response = Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }); + match transaction_status { + Some(status) => Self { + response, + status, + ..item.data + }, + None => Self { + response, + ..item.data + }, + } + } +} + +fn get_error_response_if_failure( + (info_response, status, http_code): ( + &BankOfAmericaClientReferenceResponse, + enums::AttemptStatus, + u16, + ), +) -> Option { + if utils::is_payment_failure(status) { + Some(types::ErrorResponse::from(( + &info_response.error_information, + &info_response.risk_information, + http_code, + info_response.id.clone(), + ))) + } else { + None + } +} + +fn get_payment_response( + (info_response, status, http_code): ( + &BankOfAmericaClientReferenceResponse, + enums::AttemptStatus, + u16, + ), +) -> Result { + let error_response = get_error_response_if_failure((info_response, status, http_code)); + match error_response { + Some(error) => Err(error), + None => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), + redirection_data: None, + mandate_reference: None, + connector_metadata: info_response + .processor_information + .as_ref() + .map(|processor_information| serde_json::json!({"avs_response": processor_information.avs})), + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .clone() + .unwrap_or(info_response.id.clone()), + ), + incremental_authorization_allowed: None, + }), + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaAuthSetupResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaAuthSetupResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaAuthSetupResponse::ClientAuthSetupInfo(info_response) => Ok(Self { + status: enums::AttemptStatus::AuthenticationPending, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: Some(services::RedirectForm::CybersourceAuthSetup { + access_token: info_response + .consumer_authentication_information + .access_token, + ddc_url: info_response + .consumer_authentication_information + .device_data_collection_url, + reference_id: info_response + .consumer_authentication_information + .reference_id, + }), + 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.clone()), ), + incremental_authorization_allowed: None, + }), + ..item.data + }), + BankOfAmericaAuthSetupResponse::ErrorInformation(error_response) => { + let error_reason = error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason; + Ok(Self { + response: Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }), + status: enums::AttemptStatus::AuthenticationFailed, + ..item.data + }) + } + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthInformationRequest { + return_url: String, + reference_id: String, +} +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaAuthEnrollmentRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: BankOfAmericaConsumerAuthInformationRequest, + order_information: OrderInformationWithBill, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct BankOfAmericaRedirectionAuthResponse { + pub transaction_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthInformationValidateRequest { + authentication_transaction_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaAuthValidateRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: BankOfAmericaConsumerAuthInformationValidateRequest, + order_information: OrderInformation, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum BankOfAmericaPreProcessingRequest { + AuthEnrollment(BankOfAmericaAuthEnrollmentRequest), + AuthValidate(BankOfAmericaAuthValidateRequest), +} + +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsPreProcessingRouterData>> + for BankOfAmericaPreProcessingRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankOfAmericaRouterData<&types::PaymentsPreProcessingRouterData>, + ) -> Result { + let client_reference_information = ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }; + let payment_method_data = item.router_data.request.payment_method_data.clone().ok_or( + errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "payment_method_data", + }, + )?; + let payment_information = match payment_method_data { + payments::PaymentMethodData::Card(ccard) => { + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, }; + Ok(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, + }, + })) + } + payments::PaymentMethodData::Wallet(_) + | 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("BankOfAmerica"), + )) + } + }?; - let client_reference_information = ClientReferenceInformation { - code: Some(item.router_data.connector_request_reference_id.clone()), + let redirect_response = item.router_data.request.redirect_response.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "redirect_response", + }, + )?; + + let amount_details = Amount { + total_amount: item.amount.clone(), + currency: item.router_data.request.currency.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "currency", + }, + )?, + }; + + match redirect_response.params { + Some(param) if !param.clone().peek().is_empty() => { + let reference_id = param + .clone() + .peek() + .split_once('=') + .ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "request.redirect_response.params.reference_id", + })? + .1 + .to_string(); + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill { + amount_details, + bill_to, }; - - Ok(Self { - processing_information, + Ok(Self::AuthEnrollment(BankOfAmericaAuthEnrollmentRequest { payment_information, + client_reference_information, + consumer_authentication_information: + BankOfAmericaConsumerAuthInformationRequest { + return_url: item.router_data.request.get_complete_authorize_url()?, + reference_id, + }, order_information, + })) + } + Some(_) | None => { + let redirect_payload: BankOfAmericaRedirectionAuthResponse = redirect_response + .payload + .ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "request.redirect_response.payload", + })? + .peek() + .clone() + .parse_value("BankOfAmericaRedirectionAuthResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let order_information = OrderInformation { amount_details }; + Ok(Self::AuthValidate(BankOfAmericaAuthValidateRequest { + payment_information, client_reference_information, - }) + consumer_authentication_information: + BankOfAmericaConsumerAuthInformationValidateRequest { + authentication_transaction_id: redirect_payload.transaction_id, + }, + order_information, + })) } - payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::Wallet(_) + } + } +} + +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + ) -> Result { + let payment_method_data = item.router_data.request.payment_method_data.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "payment_method_data", + }, + )?; + match payment_method_data { + payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::CardRedirect(_) | payments::PaymentMethodData::PayLater(_) | payments::PaymentMethodData::BankRedirect(_) | payments::PaymentMethodData::BankDebit(_) @@ -246,9 +1299,10 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Bank of America"), + utils::get_unimplemented_payment_method_error_message("BankOfAmerica"), ) .into()) } @@ -258,124 +1312,260 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> #[derive(Debug, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum BankofamericaPaymentStatus { - Authorized, - Succeeded, - Failed, - Voided, - Reversed, - Pending, - Declined, - AuthorizedPendingReview, - Transmitted, +pub enum BankOfAmericaAuthEnrollmentStatus { + PendingAuthentication, + AuthenticationSuccessful, + AuthenticationFailed, +} +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthValidateResponse { + ucaf_collection_indicator: Option, + cavv: Option, + ucaf_authentication_data: Option, + xid: Option, + specification_version: Option, + directory_server_transaction_id: Option, + indicator: Option, } -impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { - fn foreign_from((status, auto_capture): (BankofamericaPaymentStatus, bool)) -> Self { - match status { - BankofamericaPaymentStatus::Authorized => { - if auto_capture { - // Because BankOfAmerica will return Payment Status as Authorized even in AutoCapture Payment - Self::Pending - } else { - Self::Authorized - } - } - BankofamericaPaymentStatus::AuthorizedPendingReview => Self::Authorized, - BankofamericaPaymentStatus::Succeeded | BankofamericaPaymentStatus::Transmitted => { - Self::Charged - } - BankofamericaPaymentStatus::Voided | BankofamericaPaymentStatus::Reversed => { - Self::Voided - } - BankofamericaPaymentStatus::Failed | BankofamericaPaymentStatus::Declined => { - Self::Failure - } - BankofamericaPaymentStatus::Pending => Self::Pending, - } - } +#[derive(Debug, Deserialize, Serialize)] +pub struct BankOfAmericaThreeDSMetadata { + three_ds_data: BankOfAmericaConsumerAuthValidateResponse, } #[derive(Debug, Deserialize)] -#[serde(untagged)] -pub enum BankOfAmericaPaymentsResponse { - ClientReferenceInformation(BankOfAmericaClientReferenceResponse), - ErrorInformation(BankOfAmericaErrorInformationResponse), +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthInformationEnrollmentResponse { + access_token: Option, + step_up_url: Option, + //Added to segregate the three_ds_data in a separate struct + #[serde(flatten)] + validate_response: BankOfAmericaConsumerAuthValidateResponse, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct BankOfAmericaClientReferenceResponse { +pub struct ClientAuthCheckInfoResponse { id: String, - status: BankofamericaPaymentStatus, client_reference_information: ClientReferenceInformation, + consumer_authentication_information: BankOfAmericaConsumerAuthInformationEnrollmentResponse, + status: BankOfAmericaAuthEnrollmentStatus, + error_information: Option, } #[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BankOfAmericaErrorInformationResponse { - id: String, - error_information: BankOfAmericaErrorInformation, +#[serde(untagged)] +pub enum BankOfAmericaPreProcessingResponse { + ClientAuthCheckInfo(Box), + ErrorInformation(BankOfAmericaErrorInformationResponse), } -#[derive(Debug, Deserialize)] -pub struct BankOfAmericaErrorInformation { - reason: Option, - message: String, +impl From for enums::AttemptStatus { + fn from(item: BankOfAmericaAuthEnrollmentStatus) -> Self { + match item { + BankOfAmericaAuthEnrollmentStatus::PendingAuthentication => Self::AuthenticationPending, + BankOfAmericaAuthEnrollmentStatus::AuthenticationSuccessful => { + Self::AuthenticationSuccessful + } + BankOfAmericaAuthEnrollmentStatus::AuthenticationFailed => Self::AuthenticationFailed, + } + } } impl TryFrom< types::ResponseRouterData< F, - BankOfAmericaPaymentsResponse, - types::PaymentsAuthorizeData, + BankOfAmericaPreProcessingResponse, + types::PaymentsPreProcessingData, types::PaymentsResponseData, >, - > for types::RouterData + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, - BankOfAmericaPaymentsResponse, - types::PaymentsAuthorizeData, + BankOfAmericaPreProcessingResponse, + types::PaymentsPreProcessingData, types::PaymentsResponseData, >, ) -> Result { 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( + BankOfAmericaPreProcessingResponse::ClientAuthCheckInfo(info_response) => { + let status = enums::AttemptStatus::from(info_response.status); + let risk_info: Option = None; + if utils::is_payment_failure(status) { + let response = Err(types::ErrorResponse::from(( + &info_response.error_information, + &risk_info, + item.http_code, info_response.id.clone(), - ), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: Some( + ))); + + Ok(Self { + status, + response, + ..item.data + }) + } else { + let connector_response_reference_id = Some( info_response .client_reference_information .code - .unwrap_or(info_response.id), - ), - }), - ..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, - reason: error_response.error_information.reason, + .unwrap_or(info_response.id.clone()), + ); + + let redirection_data = match ( + info_response + .consumer_authentication_information + .access_token, + info_response + .consumer_authentication_information + .step_up_url, + ) { + (Some(access_token), Some(step_up_url)) => { + Some(services::RedirectForm::CybersourceConsumerAuth { + access_token, + step_up_url, + }) + } + _ => None, + }; + let three_ds_data = serde_json::to_value( + info_response + .consumer_authentication_information + .validate_response, + ) + .into_report() + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data, + mandate_reference: None, + connector_metadata: Some( + serde_json::json!({"three_ds_data":three_ds_data}), + ), + network_txn_id: None, + connector_response_reference_id, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } + } + BankOfAmericaPreProcessingResponse::ErrorInformation(ref error_response) => { + let error_reason = error_response + .error_information + .message + .to_owned() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason.to_owned(); + let response = Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), status_code: item.http_code, attempt_status: None, - }), - ..item.data - }), + connector_transaction_id: Some(error_response.id.clone()), + }); + Ok(Self { + response, + status: enums::AttemptStatus::AuthenticationFailed, + ..item.data + }) + } + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => { + let status = enums::AttemptStatus::foreign_from(( + info_response.status.clone(), + item.data.request.is_auto_capture()?, + )); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from(( + &error_response.clone(), + item, + Some(enums::AttemptStatus::Failure), + ))) + } + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => { + let status = enums::AttemptStatus::foreign_from(( + info_response.status.clone(), + item.data.request.is_auto_capture()?, + )); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from(( + &error_response.clone(), + item, + Some(enums::AttemptStatus::Failure), + ))) + } } } } @@ -400,35 +1590,19 @@ impl >, ) -> 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), - ), - }), - ..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, - reason: error_response.error_information.reason, - status_code: item.http_code, - attempt_status: None, - }), - ..item.data - }), + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => { + let status = + enums::AttemptStatus::foreign_from((info_response.status.clone(), true)); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item, None))) + } } } } @@ -453,35 +1627,19 @@ impl >, ) -> 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), - ), - }), - ..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, - reason: error_response.error_information.reason, - status_code: item.http_code, - attempt_status: None, - }), - ..item.data - }), + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => { + let status = + enums::AttemptStatus::foreign_from((info_response.status.clone(), false)); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item, None))) + } } } } @@ -499,6 +1657,7 @@ pub struct BankOfAmericaApplicationInfoResponse { id: String, application_information: ApplicationInformation, client_reference_information: Option, + error_information: Option, } #[derive(Debug, Deserialize)] @@ -527,24 +1686,44 @@ impl >, ) -> Result { match item.response { - BankOfAmericaTransactionResponse::ApplicationInformation(app_response) => Ok(Self { - status: enums::AttemptStatus::foreign_from(( + BankOfAmericaTransactionResponse::ApplicationInformation(app_response) => { + let 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)), - }), - ..item.data - }), + )); + let risk_info: Option = None; + if utils::is_payment_failure(status) { + Ok(Self { + response: Err(types::ErrorResponse::from(( + &app_response.error_information, + &risk_info, + item.http_code, + app_response.id.clone(), + ))), + status: enums::AttemptStatus::Failure, + ..item.data + }) + } else { + Ok(Self { + status, + 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 { @@ -556,6 +1735,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(error_response.id), + incremental_authorization_allowed: None, }), ..item.data }), @@ -574,6 +1754,8 @@ pub struct OrderInformation { pub struct BankOfAmericaCaptureRequest { order_information: OrderInformation, client_reference_information: ClientReferenceInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, } impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> @@ -583,6 +1765,10 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> fn try_from( value: &BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>, ) -> Result { + let merchant_defined_information = + value.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { order_information: OrderInformation { amount_details: Amount { @@ -593,6 +1779,7 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> client_reference_information: ClientReferenceInformation { code: Some(value.router_data.connector_request_reference_id.clone()), }, + merchant_defined_information, }) } } @@ -602,6 +1789,9 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> pub struct BankOfAmericaVoidRequest { client_reference_information: ClientReferenceInformation, reversal_information: ReversalInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, + // The connector documentation does not mention the merchantDefinedInformation field for Void requests. But this has been still added because it works! } #[derive(Debug, Serialize)] @@ -618,6 +1808,10 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCancelRouterData>> fn try_from( value: &BankOfAmericaRouterData<&types::PaymentsCancelRouterData>, ) -> Result { + let merchant_defined_information = + value.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { client_reference_information: ClientReferenceInformation { code: Some(value.router_data.connector_request_reference_id.clone()), @@ -640,6 +1834,7 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCancelRouterData>> field_name: "Cancellation Reason", })?, }, + merchant_defined_information, }) } } @@ -678,7 +1873,7 @@ impl From for enums::RefundStatus { BankofamericaRefundStatus::Succeeded | BankofamericaRefundStatus::Transmitted => { Self::Success } - BankofamericaRefundStatus::Failed => Self::Failure, + BankofamericaRefundStatus::Failed | BankofamericaRefundStatus::Voided => Self::Failure, BankofamericaRefundStatus::Pending => Self::Pending, } } @@ -715,6 +1910,7 @@ pub enum BankofamericaRefundStatus { Transmitted, Failed, Pending, + Voided, } #[derive(Debug, Deserialize)] @@ -751,34 +1947,42 @@ impl TryFrom, pub status: Option, pub message: Option, - pub reason: Option, + pub reason: Option, pub details: Option>, } -#[derive(Debug, Deserialize, strum::Display)] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaServerErrorResponse { + pub status: Option, + pub message: Option, + pub reason: Option, +} + +#[derive(Debug, Deserialize)] #[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)] +pub struct BankOfAmericaAuthenticationErrorResponse { + pub response: AuthenticationErrorInformation, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum BankOfAmericaErrorResponse { + AuthenticationError(BankOfAmericaAuthenticationErrorResponse), + StandardError(BankOfAmericaStandardErrorResponse), +} + #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Details { @@ -791,3 +1995,62 @@ pub struct ErrorInformation { pub message: String, pub reason: String, } + +#[derive(Debug, Default, Deserialize)] +pub struct AuthenticationErrorInformation { + pub rmsg: String, +} + +impl + From<( + &Option, + &Option, + u16, + String, + )> for types::ErrorResponse +{ + fn from( + (error_data, risk_information, status_code, transaction_id): ( + &Option, + &Option, + u16, + String, + ), + ) -> Self { + let avs_message = risk_information + .clone() + .map(|client_risk_information| { + client_risk_information.rules.map(|rules| { + rules + .iter() + .map(|risk_info| format!(" , {}", risk_info.name)) + .collect::>() + .join("") + }) + }) + .unwrap_or(Some("".to_string())); + let error_reason = error_data + .clone() + .map(|error_details| { + error_details.message.unwrap_or("".to_string()) + + &avs_message.unwrap_or("".to_string()) + }) + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_data + .clone() + .and_then(|error_details| error_details.reason); + + Self { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message + .clone() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason.clone()), + status_code, + attempt_status: Some(enums::AttemptStatus::Failure), + connector_transaction_id: Some(transaction_id.clone()), + } + } +} diff --git a/crates/router/src/connector/bitpay.rs b/crates/router/src/connector/bitpay.rs index dc4571b75746..4a8108d04b83 100644 --- a/crates/router/src/connector/bitpay.rs +++ b/crates/router/src/connector/bitpay.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::{errors::ReportSwitchExt, ext_traits::ByteSliceExt}; +use common_utils::{errors::ReportSwitchExt, ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::ResultExt; use masking::PeekInterface; use transformers as bitpay; @@ -23,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt, Encode}, + utils::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, }) } } @@ -145,6 +146,20 @@ impl types::PaymentsResponseData, > for Bitpay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Bitpay".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -174,21 +189,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bitpay::BitpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = bitpay::BitpayPaymentsRequest::try_from(&connector_router_data)?; + let connector_req = bitpay::BitpayPaymentsRequest::try_from(&connector_router_data)?; - let bitpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bitpay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -206,7 +216,7 @@ impl ConnectorIntegration, - ) -> 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..e54d8320d0ff 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -6,6 +6,7 @@ use base64::Engine; use common_utils::{ crypto, ext_traits::{StringExt, ValueExt}, + request::RequestContent, }; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; @@ -34,7 +35,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; pub const BLUESNAP_TRANSACTION_NOT_FOUND: &str = "is not authorized to view merchant-transaction:"; @@ -127,6 +128,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 +137,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 +159,7 @@ impl ConnectorCommon for Bluesnap { message: error_response, reason: Some(error_res), attempt_status, + connector_transaction_id: None, } } }; @@ -229,6 +233,20 @@ impl types::PaymentsResponseData, > for Bluesnap { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Bluesnap".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Bluesnap {} @@ -264,14 +282,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = bluesnap::BluesnapVoidRequest::try_from(req)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -284,7 +297,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -449,12 +462,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -469,7 +477,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = bluesnap::BluesnapCreateWalletToken::try_from(req)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -559,7 +562,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -643,22 +646,12 @@ impl ConnectorIntegration { let connector_req = bluesnap::BluesnapPaymentsTokenRequest::try_from(&connector_router_data)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } _ => { let connector_req = bluesnap::BluesnapPaymentsRequest::try_from(&connector_router_data)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } } } @@ -678,7 +671,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -779,12 +773,7 @@ impl ))?; let connector_req = bluesnap::BluesnapCompletePaymentsRequest::try_from(&connector_router_data)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -801,7 +790,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -869,7 +858,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -877,12 +866,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -897,7 +881,7 @@ impl ConnectorIntegration { - Ok(api_models::webhooks::ObjectReferenceId::PaymentId( - api_models::payments::PaymentIdType::PaymentAttemptId( - webhook_body.merchant_transaction_id, - ), - )) + if webhook_body.merchant_transaction_id.is_empty() { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + webhook_body.reference_number, + ), + )) + } else { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + webhook_body.merchant_transaction_id, + ), + )) + } } bluesnap::BluesnapWebhookEvents::Refund => { Ok(api_models::webhooks::ObjectReferenceId::RefundId( @@ -1119,15 +1111,13 @@ impl api::IncomingWebhook for Bluesnap { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> 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..e98b98e874aa 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use api_models::{enums as api_enums, payments}; use base64::Engine; use common_utils::{ @@ -8,6 +10,7 @@ use common_utils::{ use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use crate::{ connector::utils::{ @@ -17,10 +20,16 @@ use crate::{ consts, core::errors, pii::Secret, - types::{self, api, storage::enums, transformers::ForeignTryFrom}, + types::{ + self, api, + storage::enums, + transformers::{ForeignFrom, ForeignTryFrom}, + }, utils::{Encode, OptionExt}, }; +const DISPLAY_METADATA: &str = "Y"; + #[derive(Debug, Serialize)] pub struct BluesnapRouterData { pub amount: String, @@ -63,6 +72,21 @@ pub struct BluesnapPaymentsRequest { transaction_fraud_info: Option, card_holder_info: Option, merchant_transaction_id: Option, + transaction_meta_data: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapMetadata { + meta_data: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestMetadata { + meta_key: Option, + meta_value: Option, + is_visible: Option, } #[derive(Debug, Serialize)] @@ -221,7 +245,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 +265,169 @@ 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 transaction_meta_data = + item.router_data + .request + .metadata + .as_ref() + .map(|metadata| BluesnapMetadata { + meta_data: Vec::::foreign_from(metadata.peek().to_owned()), + }); + + 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, @@ -404,6 +438,7 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> for Blues }), card_holder_info, merchant_transaction_id: Some(item.router_data.connector_request_reference_id.clone()), + transaction_meta_data, }) } } @@ -528,7 +563,7 @@ impl TryFrom, card_holder_info: Option, merchant_transaction_id: Option, + transaction_meta_data: Option, } impl TryFrom<&BluesnapRouterData<&types::PaymentsCompleteAuthorizeRouterData>> @@ -589,6 +625,15 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsCompleteAuthorizeRouterData>> .parse_value("BluesnapRedirectionResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let transaction_meta_data = + item.router_data + .request + .metadata + .as_ref() + .map(|metadata| BluesnapMetadata { + meta_data: Vec::::foreign_from(metadata.peek().to_owned()), + }); + let pf_token = item .router_data .request @@ -636,6 +681,7 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsCompleteAuthorizeRouterData>> )?, merchant_transaction_id: Some(item.router_data.connector_request_reference_id.clone()), pf_token, + transaction_meta_data, }) } } @@ -855,6 +901,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -1019,7 +1066,7 @@ pub struct BluesnapWebhookObjectResource { reversal_ref_num: Option, } -impl TryFrom for serde_json::Value { +impl TryFrom for Value { type Error = error_stack::Report; fn try_from(details: BluesnapWebhookObjectResource) -> Result { let (card_transaction_type, processing_status, transaction_id) = match details @@ -1116,3 +1163,19 @@ impl From for utils::ErrorCodeAndMessage { } } } + +impl ForeignFrom for Vec { + fn foreign_from(metadata: Value) -> Self { + let hashmap: HashMap, Option> = + serde_json::from_str(&metadata.to_string()).unwrap_or(HashMap::new()); + let mut vector: Self = Self::new(); + for (key, value) in hashmap { + vector.push(RequestMetadata { + meta_key: key, + meta_value: value.map(|field_value| field_value.to_string()), + is_visible: Some(DISPLAY_METADATA.to_string()), + }); + } + vector + } +} diff --git a/crates/router/src/connector/boku.rs b/crates/router/src/connector/boku.rs index 7c2c1af0986b..39566e08d470 100644 --- a/crates/router/src/connector/boku.rs +++ b/crates/router/src/connector/boku.rs @@ -1,8 +1,7 @@ pub mod transformers; - use std::fmt::Debug; -use common_utils::ext_traits::XmlExt; +use common_utils::{ext_traits::XmlExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, Report, ResultExt}; use masking::{ExposeInterface, PeekInterface, Secret, WithType}; @@ -11,9 +10,9 @@ use roxmltree; use time::OffsetDateTime; use transformers as boku; +use super::utils as connector_utils; use crate::{ configs::settings, - connector::utils as connector_utils, consts, core::errors::{self, CustomResult}, headers, logger, @@ -28,7 +27,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt, OptionExt}, + utils::{BytesExt, OptionExt}, }; #[derive(Debug, Clone)] @@ -131,6 +130,7 @@ impl ConnectorCommon for Boku { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }), Err(_) => get_xml_deserialized(res), } @@ -170,6 +170,20 @@ impl types::PaymentsResponseData, > for Boku { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Boku".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -208,14 +222,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = boku::BokuPaymentsRequest::try_from(req)?; - let boku_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_xml, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(boku_req)) + ) -> CustomResult { + let connector_req = boku::BokuPaymentsRequest::try_from(req)?; + Ok(RequestContent::Xml(Box::new(connector_req))) } fn build_request( @@ -233,7 +242,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = boku::BokuPsyncRequest::try_from(req)?; - let boku_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_xml, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(boku_req)) + ) -> CustomResult { + let connector_req = boku::BokuPsyncRequest::try_from(req)?; + Ok(RequestContent::Xml(Box::new(connector_req))) } fn build_request( @@ -322,7 +326,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -402,7 +406,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = boku::BokuRefundRequest::try_from(req)?; - let boku_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_xml, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(boku_req)) + ) -> CustomResult { + let connector_req = boku::BokuRefundRequest::try_from(req)?; + Ok(RequestContent::Xml(Box::new(connector_req))) } fn build_request( @@ -495,7 +494,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = boku::BokuRsyncRequest::try_from(req)?; - let boku_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_xml, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(boku_req)) + ) -> CustomResult { + let connector_req = boku::BokuRsyncRequest::try_from(req)?; + Ok(RequestContent::Xml(Box::new(connector_req))) } fn build_request( @@ -577,7 +571,7 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } @@ -668,6 +662,7 @@ fn get_xml_deserialized(res: Response) -> CustomResult for BokuPaymentsRequest { api_models::payments::PaymentMethodData::Wallet(wallet_data) => { Self::try_from((item, &wallet_data)) } - _ => Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.request.payment_method_type), - connector: "Boku", - })?, + api_models::payments::PaymentMethodData::Card(_) + | api_models::payments::PaymentMethodData::CardRedirect(_) + | api_models::payments::PaymentMethodData::PayLater(_) + | api_models::payments::PaymentMethodData::BankRedirect(_) + | api_models::payments::PaymentMethodData::BankDebit(_) + | api_models::payments::PaymentMethodData::BankTransfer(_) + | api_models::payments::PaymentMethodData::Crypto(_) + | api_models::payments::PaymentMethodData::MandatePayment + | api_models::payments::PaymentMethodData::Reward + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::Voucher(_) + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("boku"), + ))? + } } } } @@ -136,9 +149,31 @@ fn get_wallet_type(wallet_data: &api::WalletData) -> Result { Ok(BokuPaymentType::Kakaopay.to_string()) } - _ => Err(errors::ConnectorError::NotImplemented( - "Payment method".to_string(), - )), + api_models::payments::WalletData::AliPayQr(_) + | api_models::payments::WalletData::AliPayRedirect(_) + | api_models::payments::WalletData::AliPayHkRedirect(_) + | api_models::payments::WalletData::ApplePay(_) + | api_models::payments::WalletData::ApplePayRedirect(_) + | api_models::payments::WalletData::ApplePayThirdPartySdk(_) + | api_models::payments::WalletData::GooglePay(_) + | api_models::payments::WalletData::GooglePayRedirect(_) + | api_models::payments::WalletData::GooglePayThirdPartySdk(_) + | api_models::payments::WalletData::MbWayRedirect(_) + | api_models::payments::WalletData::MobilePayRedirect(_) + | api_models::payments::WalletData::PaypalRedirect(_) + | api_models::payments::WalletData::PaypalSdk(_) + | api_models::payments::WalletData::SamsungPay(_) + | api_models::payments::WalletData::TwintRedirect {} + | api_models::payments::WalletData::VippsRedirect {} + | api_models::payments::WalletData::TouchNGoRedirect(_) + | api_models::payments::WalletData::WeChatPayRedirect(_) + | api_models::payments::WalletData::WeChatPayQr(_) + | api_models::payments::WalletData::CashappQr(_) + | api_models::payments::WalletData::SwishQr(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("boku"), + )) + } } } @@ -252,6 +287,7 @@ impl TryFrom Ok(ErrorResponse { @@ -141,6 +141,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); @@ -242,7 +243,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = braintree::BraintreeSessionRequest::try_from(req)?; - let braintree_session_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_session_request)) + ) -> CustomResult { + let connector_req = braintree::BraintreeSessionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -328,16 +324,10 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = - braintree_graphql_transformers::BraintreeTokenRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = braintree_graphql_transformers::BraintreeTokenRequest::try_from(req)?; - let braintree_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -353,7 +343,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -400,6 +390,20 @@ impl > for Braintree { // Not Implemented (R) + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Braintree".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -446,7 +450,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version.clone(); let connector_router_data = braintree_graphql_transformers::BraintreeRouterData::try_from(( @@ -457,17 +461,12 @@ impl ConnectorIntegration { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreeCaptureRequest::try_from( &connector_router_data, )?; - let braintree_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } false => Err(errors::ConnectorError::NotImplemented( "get_request_body method".to_string(), @@ -491,7 +490,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { true => { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreePSyncRequest::try_from(req)?; - let braintree_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } - false => Ok(None), + false => Err(errors::ConnectorError::RequestEncodingFailed).into_report(), } } @@ -630,7 +624,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version; let connector_router_data = braintree_graphql_transformers::BraintreeRouterData::try_from(( @@ -786,25 +780,15 @@ impl ConnectorIntegration { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreePaymentsRequest::try_from( &connector_router_data, )?; - let braintree_payment_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_payment_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } false => { - let connector_request = braintree::BraintreePaymentsRequest::try_from(req)?; - let braintree_payment_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_payment_request)) + let connector_req = braintree::BraintreePaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } } } @@ -937,7 +921,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { true => { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreeCancelRequest::try_from(req)?; - let braintree_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } - false => Ok(None), + false => Err(errors::ConnectorError::RequestEncodingFailed).into_report(), } } @@ -1078,7 +1057,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version; let connector_router_data = braintree_graphql_transformers::BraintreeRouterData::try_from(( @@ -1089,25 +1068,15 @@ impl ConnectorIntegration { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreeRefundRequest::try_from( connector_router_data, )?; - let braintree_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_refund_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } false => { - let connector_request = braintree::BraintreeRefundRequest::try_from(req)?; - let braintree_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_refund_request)) + let connector_req = braintree::BraintreeRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } } } @@ -1124,7 +1093,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { true => { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreeRSyncRequest::try_from(req)?; - let braintree_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_refund_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } - false => Ok(None), + false => Err(errors::ConnectorError::RequestEncodingFailed).into_report(), } } @@ -1245,7 +1209,7 @@ impl ConnectorIntegration, - ) -> 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( @@ -1613,7 +1573,7 @@ impl &self, req: &types::PaymentsCompleteAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = braintree_graphql_transformers::BraintreeRouterData::try_from(( &self.get_currency_unit(), @@ -1624,22 +1584,18 @@ impl let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { true => { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreePaymentsRequest::try_from( &connector_router_data, )?; - let braintree_payment_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_payment_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } false => Err(errors::ConnectorError::NotImplemented( "get_request_body method".to_string(), ))?, } } + fn build_request( &self, req: &types::PaymentsCompleteAuthorizeRouterData, @@ -1657,7 +1613,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index bf51973237c5..e0baf034f72a 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 }), @@ -294,11 +297,11 @@ fn build_error_response( get_error_response( response - .get(0) + .first() .and_then(|err_details| err_details.extensions.as_ref()) .and_then(|extensions| extensions.legacy_code.clone()), response - .get(0) + .first() .map(|err_details| err_details.message.clone()), reason, http_code, @@ -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 }) @@ -859,7 +867,9 @@ impl TryFrom<&types::TokenizationRouterData> for BraintreeTokenRequest { expiration_year: card_data.card_exp_year, expiration_month: card_data.card_exp_month, cvv: card_data.card_cvc, - cardholder_name: card_data.card_holder_name, + cardholder_name: card_data + .card_holder_name + .unwrap_or(Secret::new("".to_string())), }, }; Ok(Self { @@ -878,12 +888,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 +1069,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1157,6 +1167,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1254,6 +1265,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1422,9 +1434,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..f4bd62add3b9 100644 --- a/crates/router/src/connector/braintree/transformers.rs +++ b/crates/router/src/connector/braintree/transformers.rs @@ -228,17 +228,17 @@ impl types::PaymentsResponseData, >, ) -> Result { + let id = item.response.transaction.id.clone(); Ok(Self { status: enums::AttemptStatus::from(item.response.transaction.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transaction.id, - ), + resource_id: types::ResponseId::ConnectorTransactionId(id.clone()), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index 12a52e485396..5a628f775b6b 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -1,16 +1,16 @@ pub mod transformers; - use std::fmt::Debug; use base64::Engine; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; use transformers as cashtocode; +use super::utils as connector_utils; use crate::{ configs::settings::{self}, - connector::{utils as connector_utils, utils as conn_utils}, core::errors::{self, CustomResult}, headers, services::{ @@ -23,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, domain, storage, ErrorResponse, Response, }, - utils::{self, ByteSliceExt, BytesExt}, + utils::{ByteSliceExt, BytesExt}, }; #[derive(Debug, Clone)] @@ -120,6 +120,7 @@ impl ConnectorCommon for Cashtocode { message: response.error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -157,6 +158,20 @@ impl types::PaymentsResponseData, > for Cashtocode { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Cashtocode".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -204,14 +219,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = cashtocode::CashtocodePaymentsRequest::try_from(req)?; - let cashtocode_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cashtocode_req)) + ) -> CustomResult { + let connector_req = cashtocode::CashtocodePaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -229,7 +239,7 @@ impl ConnectorIntegration, _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let base64_signature = conn_utils::get_header_key_value("authorization", request.headers)?; + let base64_signature = + connector_utils::get_header_key_value("authorization", request.headers)?; let signature = base64_signature.as_bytes().to_owned(); Ok(signature) } @@ -391,16 +402,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..8a92956756a8 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; -use common_utils::{ext_traits::ValueExt, pii::Email}; +pub use common_utils::request::Method; +use common_utils::{errors::CustomResult, ext_traits::ValueExt, pii::Email}; use error_stack::{IntoReport, ResultExt}; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -185,27 +186,54 @@ pub enum CashtocodePaymentsResponse { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CashtocodePaymentsResponseData { - pub pay_url: String, + pub pay_url: url::Url, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CashtocodePaymentsSyncResponse { pub transaction_id: String, - pub amount: i64, + pub amount: f64, } -impl +fn get_redirect_form_data( + payment_method_type: &enums::PaymentMethodType, + response_data: CashtocodePaymentsResponseData, +) -> CustomResult { + match payment_method_type { + enums::PaymentMethodType::ClassicReward => Ok(services::RedirectForm::Form { + //redirect form is manually constructed because the connector for this pm type expects query params in the url + endpoint: response_data.pay_url.to_string(), + method: services::Method::Post, + form_fields: Default::default(), + }), + enums::PaymentMethodType::Evoucher => Ok(services::RedirectForm::from(( + //here the pay url gets parsed, and query params are sent as formfields as the connector expects + response_data.pay_url, + services::Method::Get, + ))), + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("CashToCode"), + ))?, + } +} + +impl TryFrom< - types::ResponseRouterData, - > for types::RouterData + types::ResponseRouterData< + F, + CashtocodePaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, CashtocodePaymentsResponse, - T, + types::PaymentsAuthorizeData, types::PaymentsResponseData, >, ) -> Result { @@ -218,14 +246,17 @@ impl message: error_data.error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }), ), CashtocodePaymentsResponse::CashtoCodeData(response_data) => { - let redirection_data = services::RedirectForm::Form { - endpoint: response_data.pay_url, - method: services::Method::Post, - form_fields: Default::default(), - }; + let payment_method_type = item + .data + .request + .payment_method_type + .as_ref() + .ok_or(errors::ConnectorError::MissingPaymentMethodType)?; + let redirection_data = get_redirect_form_data(payment_method_type, response_data)?; ( enums::AttemptStatus::AuthenticationPending, Ok(types::PaymentsResponseData::TransactionResponse { @@ -237,6 +268,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ) } @@ -270,18 +302,18 @@ impl >, ) -> Result { Ok(Self { - status: enums::AttemptStatus::Charged, + status: enums::AttemptStatus::Charged, // Charged status is hardcoded because cashtocode do not support Psync, and we only receive webhooks when payment is succeeded, this tryFrom is used for CallConnectorAction. response: Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - item.data.attempt_id.clone(), + item.data.attempt_id.clone(), //in response they only send PayUrl, so we use attempt_id as connector_transaction_id ), redirection_data: None, mandate_reference: None, 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 +321,7 @@ impl #[derive(Debug, Deserialize)] pub struct CashtocodeErrorResponse { - pub error: String, + pub error: serde_json::Value, pub error_description: String, pub errors: Option>, } @@ -297,7 +329,7 @@ pub struct CashtocodeErrorResponse { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CashtocodeIncomingWebhook { - pub amount: i64, + pub amount: f64, pub currency: String, pub foreign_transaction_id: String, #[serde(rename = "type")] diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index f24c08233ed7..382737a28f67 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::{crypto, ext_traits::ByteSliceExt}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; @@ -29,7 +29,8 @@ use crate::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, }, - utils::{self, BytesExt}, + utils, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -132,6 +133,7 @@ impl ConnectorCommon for Checkout { .map(|errors| errors.join(" & ")) .or(response.error_type), attempt_status: None, + connector_transaction_id: None, }) } } @@ -209,14 +211,9 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = checkout::TokenRequest::try_from(req)?; - let checkout_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(checkout_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -230,7 +227,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -289,6 +286,20 @@ impl > for Checkout { // Issue: #173 + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Checkout".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -317,7 +328,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -325,12 +336,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(checkout_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -346,7 +352,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -515,12 +518,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(checkout_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -541,7 +539,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = checkout::PaymentVoidRequest::try_from(req)?; - let checkout_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(checkout_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -621,7 +614,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -699,12 +692,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(body)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -719,7 +707,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let checkout_req = transformers::construct_file_upload_request(req.clone())?; - Ok(Some(checkout_req)) + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = transformers::construct_file_upload_request(req.clone())?; + Ok(RequestContent::FormData(connector_req)) } fn build_request( @@ -987,8 +973,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let checkout_req = checkout::Evidence::try_from(req)?; - let checkout_req_string = types::RequestBody::log_and_get_request_body( - &checkout_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(checkout_req_string)) + ) -> CustomResult { + let connector_req = checkout::Evidence::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1085,7 +1067,7 @@ impl .headers(types::SubmitEvidenceType::get_headers( self, req, connectors, )?) - .body(types::SubmitEvidenceType::get_request_body( + .set_body(types::SubmitEvidenceType::get_request_body( self, req, connectors, )?) .build(); @@ -1261,7 +1243,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 +1263,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..6e7656c38a6f 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -1,12 +1,15 @@ use common_utils::{errors::CustomResult, ext_traits::ByteSliceExt}; use error_stack::{IntoReport, ResultExt}; -use masking::{ExposeInterface, PeekInterface, Secret}; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use url::Url; use crate::{ - connector::utils::{self, PaymentsCaptureRequestData, RouterData, WalletData}, + connector::utils::{ + self, to_connector_meta, ApplePayDecrypt, PaymentsCaptureRequestData, RouterData, + WalletData, + }, consts, core::errors, services, @@ -138,7 +141,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"), ) @@ -240,6 +244,17 @@ pub struct PaymentsRequest { pub reference: String, } +#[derive(Debug, Serialize, Deserialize)] +pub struct CheckoutMeta { + pub psync_flow: CheckoutPaymentIntent, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub enum CheckoutPaymentIntent { + Capture, + Authorize, +} + #[derive(Debug, Serialize)] pub struct CheckoutThreeDS { enabled: bool, @@ -303,24 +318,8 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme })) } types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { - let expiry_year_4_digit = Secret::new(format!( - "20{}", - decrypt_data - .clone() - .application_expiration_date - .peek() - .get(0..2) - .ok_or(errors::ConnectorError::RequestEncodingFailed)? - )); - let exp_month = Secret::new( - decrypt_data - .clone() - .application_expiration_date - .peek() - .get(2..4) - .ok_or(errors::ConnectorError::RequestEncodingFailed)? - .to_owned(), - ); + let exp_month = decrypt_data.get_expiry_month()?; + let expiry_year_4_digit = decrypt_data.get_four_digit_expiry_year()?; Ok(PaymentSource::ApplePayPredecrypt(Box::new( ApplePayPredecrypt { token: decrypt_data.application_primary_account_number, @@ -375,11 +374,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 { @@ -477,7 +475,27 @@ impl ForeignFrom<(CheckoutPaymentStatus, Option)> for enum if capture_method == Some(enums::CaptureMethod::Automatic) || capture_method.is_none() { - Self::Charged + Self::Pending + } else { + Self::Authorized + } + } + CheckoutPaymentStatus::Captured => Self::Charged, + CheckoutPaymentStatus::Declined => Self::Failure, + CheckoutPaymentStatus::Pending => Self::AuthenticationPending, + CheckoutPaymentStatus::CardVerified => Self::Pending, + } + } +} + +impl ForeignFrom<(CheckoutPaymentStatus, CheckoutPaymentIntent)> for enums::AttemptStatus { + fn foreign_from(item: (CheckoutPaymentStatus, CheckoutPaymentIntent)) -> Self { + let (status, psync_flow) = item; + + match status { + CheckoutPaymentStatus::Authorized => { + if psync_flow == CheckoutPaymentIntent::Capture { + Self::Pending } else { Self::Authorized } @@ -549,6 +567,24 @@ pub struct Balances { available_to_capture: i32, } +fn get_connector_meta( + capture_method: enums::CaptureMethod, +) -> CustomResult { + match capture_method { + enums::CaptureMethod::Automatic => Ok(serde_json::json!(CheckoutMeta { + psync_flow: CheckoutPaymentIntent::Capture, + })), + enums::CaptureMethod::Manual | enums::CaptureMethod::ManualMultiple => { + Ok(serde_json::json!(CheckoutMeta { + psync_flow: CheckoutPaymentIntent::Authorize, + })) + } + enums::CaptureMethod::Scheduled => { + Err(errors::ConnectorError::CaptureMethodNotSupported.into()) + } + } +} + impl TryFrom> for types::PaymentsAuthorizeRouterData { @@ -556,6 +592,9 @@ impl TryFrom> fn try_from( item: types::PaymentsResponseRouterData, ) -> Result { + let connector_meta = + get_connector_meta(item.data.request.capture_method.unwrap_or_default())?; + let redirection_data = item.response.links.redirect.map(|href| { services::RedirectForm::from((href.redirection_url, services::Method::Get)) }); @@ -577,6 +616,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 @@ -585,11 +625,12 @@ impl TryFrom> resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data, mandate_reference: None, - connector_metadata: None, + connector_metadata: Some(connector_meta), network_txn_id: None, connector_response_reference_id: Some( item.response.reference.unwrap_or(item.response.id), ), + incremental_authorization_allowed: None, }; Ok(Self { status, @@ -609,8 +650,10 @@ impl TryFrom> let redirection_data = item.response.links.redirect.map(|href| { services::RedirectForm::from((href.redirection_url, services::Method::Get)) }); + let checkout_meta: CheckoutMeta = + to_connector_meta(item.data.request.connector_meta.clone())?; let status = - enums::AttemptStatus::foreign_from((item.response.status, item.response.balances)); + enums::AttemptStatus::foreign_from((item.response.status, checkout_meta.psync_flow)); let error_response = if status == enums::AttemptStatus::Failure { Some(types::ErrorResponse { status_code: item.http_code, @@ -625,6 +668,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 +682,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 +757,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: response.into(), ..item.data @@ -783,6 +829,9 @@ impl TryFrom> fn try_from( item: types::PaymentsCaptureResponseRouterData, ) -> Result { + let connector_meta = serde_json::json!(CheckoutMeta { + psync_flow: CheckoutPaymentIntent::Capture, + }); let (status, amount_captured) = if item.http_code == 202 { ( enums::AttemptStatus::Charged, @@ -805,9 +854,10 @@ impl TryFrom> resource_id: types::ResponseId::ConnectorTransactionId(resource_id), redirection_data: None, mandate_reference: None, - connector_metadata: None, + connector_metadata: Some(connector_meta), network_txn_id: None, connector_response_reference_id: item.response.reference, + incremental_authorization_allowed: None, }), status, amount_captured, @@ -976,10 +1026,7 @@ impl utils::MultipleCaptureSyncResponse for Box { self.status == CheckoutPaymentStatus::Captured } fn get_amount_captured(&self) -> Option { - match self.amount { - Some(amount) => amount.try_into().ok(), - None => None, - } + self.amount.map(Into::into) } } diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index 5704ea15b005..76f88b663265 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::{crypto, ext_traits::ByteSliceExt}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use transformers as coinbase; @@ -24,7 +24,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{BytesExt, Encode}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -109,6 +109,7 @@ impl ConnectorCommon for Coinbase { message: response.error.message, reason: response.error.code, attempt_status: None, + connector_transaction_id: None, }) } } @@ -156,6 +157,20 @@ impl types::PaymentsResponseData, > for Coinbase { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Coinbase".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -185,14 +200,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = coinbase::CoinbasePaymentsRequest::try_from(req)?; - let coinbase_payment_request = types::RequestBody::log_and_get_request_body( - &connector_request, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(coinbase_payment_request)) + ) -> CustomResult { + let connector_req = coinbase::CoinbasePaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -209,7 +219,7 @@ impl ConnectorIntegration, - ) -> 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..b1435e50df9d 100644 --- a/crates/router/src/connector/coinbase/transformers.rs +++ b/crates/router/src/connector/coinbase/transformers.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use common_utils::pii; +use error_stack::ResultExt; use serde::{Deserialize, Serialize}; use crate::{ @@ -146,6 +148,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{ @@ -249,6 +252,14 @@ pub struct CoinbaseConnectorMeta { pub pricing_type: String, } +impl TryFrom<&Option> for CoinbaseConnectorMeta { + type Error = error_stack::Report; + fn try_from(meta_data: &Option) -> Result { + utils::to_connector_meta_from_secret(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata" }) + } +} + fn get_crypto_specific_payment_data( item: &types::PaymentsAuthorizeRouterData, ) -> Result> { @@ -259,11 +270,10 @@ fn get_crypto_specific_payment_data( let name = billing_address.and_then(|add| add.get_first_name().ok().map(|name| name.to_owned())); let description = item.get_description().ok(); - let connector_meta: CoinbaseConnectorMeta = - utils::to_connector_meta_from_secret_with_required_field( - item.connector_meta_data.clone(), - "Pricing Type Not present in connector meta data", - )?; + let connector_meta = CoinbaseConnectorMeta::try_from(&item.connector_meta_data) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; let pricing_type = connector_meta.pricing_type; let local_price = get_local_price(item); let redirect_url = item.request.get_return_url()?; diff --git a/crates/router/src/connector/cryptopay.rs b/crates/router/src/connector/cryptopay.rs index d2d8fa0f1ec2..8727461ba34c 100644 --- a/crates/router/src/connector/cryptopay.rs +++ b/crates/router/src/connector/cryptopay.rs @@ -7,6 +7,7 @@ use common_utils::{ crypto::{self, GenerateDigest, SignMessage}, date_time, ext_traits::ByteSliceExt, + request::RequestContent, }; use error_stack::{IntoReport, ResultExt}; use hex::encode; @@ -30,7 +31,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{BytesExt, Encode}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -68,21 +69,24 @@ where req: &types::RouterData, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let api_method; - let payload = match self.get_request_body(req, connectors)? { - Some(val) => { - let body = types::RequestBody::get_inner_value(val).peek().to_owned(); - api_method = "POST".to_string(); + let method = self.get_http_method(); + let payload = match method { + common_utils::request::Method::Get => String::default(), + common_utils::request::Method::Post + | common_utils::request::Method::Put + | common_utils::request::Method::Delete + | common_utils::request::Method::Patch => { + let body = + types::RequestBody::get_inner_value(self.get_request_body(req, connectors)?) + .peek() + .to_owned(); let md5_payload = crypto::Md5 .generate_digest(body.as_bytes()) .change_context(errors::ConnectorError::RequestEncodingFailed)?; encode(md5_payload) } - None => { - api_method = "GET".to_string(); - String::default() - } }; + let api_method = method.to_string(); let now = date_time::date_as_yyyymmddthhmmssmmmz() .into_report() @@ -168,6 +172,7 @@ impl ConnectorCommon for Cryptopay { message: response.error.message, reason: response.error.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -189,6 +194,20 @@ impl types::PaymentsResponseData, > for Cryptopay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Cryptopay".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -218,21 +237,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = cryptopay::CryptopayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = - cryptopay::CryptopayPaymentsRequest::try_from(&connector_router_data)?; - let cryptopay_req = types::RequestBody::log_and_get_request_body( - &connector_request, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cryptopay_req)) + let connector_req = cryptopay::CryptopayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -250,7 +263,7 @@ impl ConnectorIntegration services::Method { + services::Method::Get + } + fn get_url( &self, req: &types::PaymentsSyncRouterData, @@ -455,13 +472,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 0bc4ff3b3ae6..4102945b201e 100644 --- a/crates/router/src/connector/cryptopay/transformers.rs +++ b/crates/router/src/connector/cryptopay/transformers.rs @@ -80,11 +80,11 @@ 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(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "CryptoPay", - }) + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("CryptoPay"), + )) } }?; Ok(cryptopay_request) @@ -172,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..7970656d6e3d 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -3,6 +3,7 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; @@ -11,6 +12,7 @@ use time::OffsetDateTime; use transformers as cybersource; use url::Url; +use super::utils::{PaymentsAuthorizeRequestData, RouterData}; use crate::{ configs::settings, connector::{utils as connector_utils, utils::RefundsRequestData}, @@ -26,7 +28,7 @@ use crate::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -53,10 +55,20 @@ impl Cybersource { api_secret, } = auth; let is_post_method = matches!(http_method, services::Method::Post); - let digest_str = if is_post_method { "digest " } else { "" }; + let is_patch_method = matches!(http_method, services::Method::Patch); + let is_delete_method = matches!(http_method, services::Method::Delete); + let digest_str = if is_post_method || is_patch_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 if is_patch_method { + format!("(request-target): patch {resource}\ndigest: SHA-256={payload}\n") + } else if is_delete_method { + format!("(request-target): delete {resource}\n") } else { format!("(request-target): get {resource}\n") }; @@ -67,7 +79,9 @@ impl Cybersource { let key_value = consts::BASE64_ENGINE .decode(api_secret.expose()) .into_report() - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "connector_account_details.api_secret", + })?; 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()); @@ -94,50 +108,89 @@ impl ConnectorCommon for Cybersource { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Minor + api::CurrencyUnit::Base } fn build_error_response( &self, res: types::Response, ) -> CustomResult { - let response: cybersource::ErrorResponse = res + let response: cybersource::CybersourceErrorResponse = res .response .parse_struct("Cybersource ErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - let details = response.details.unwrap_or_default(); - let connector_reason = details - .iter() - .map(|det| format!("{} : {}", det.field, det.reason)) - .collect::>() - .join(", "); let error_message = if res.status_code == 401 { consts::CONNECTOR_UNAUTHORIZED_ERROR } else { consts::NO_ERROR_MESSAGE }; + match response { + transformers::CybersourceErrorResponse::StandardError(response) => { + let (code, connector_reason) = 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 message = match response.details { + Some(details) => details + .iter() + .map(|det| format!("{} : {}", det.field, det.reason)) + .collect::>() + .join(", "), + None => connector_reason.clone(), + }; - 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), - ), - }; - Ok(types::ErrorResponse { - status_code: res.status_code, - code, - message, - reason: Some(connector_reason), - attempt_status: None, - }) + Ok(types::ErrorResponse { + status_code: res.status_code, + code, + message, + reason: Some(connector_reason), + attempt_status: None, + connector_transaction_id: None, + }) + } + transformers::CybersourceErrorResponse::AuthenticationError(response) => { + Ok(types::ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: response.response.rmsg.clone(), + reason: Some(response.response.rmsg), + attempt_status: None, + connector_transaction_id: None, + }) + } + transformers::CybersourceErrorResponse::NotAvailableError(response) => { + let error_response = response + .errors + .iter() + .map(|error_info| { + format!( + "{}: {}", + error_info.error_type.clone().unwrap_or("".to_string()), + error_info.message.clone().unwrap_or("".to_string()) + ) + }) + .collect::>() + .join(" & "); + Ok(types::ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: error_response.clone(), + reason: Some(error_response), + attempt_status: None, + connector_transaction_id: None, + }) + } + } } } @@ -183,10 +236,8 @@ where .skip(base_url.len() - 1) .collect(); let sha256 = self.generate_digest( - cybersource_req - .map_or("{}".to_string(), |s| { - types::RequestBody::get_inner_value(s).expose() - }) + types::RequestBody::get_inner_value(cybersource_req) + .expose() .as_bytes(), ); let http_method = self.get_http_method(); @@ -216,7 +267,10 @@ where ("Host".to_string(), host.to_string().into()), ("Signature".to_string(), signature.into_masked()), ]; - if matches!(http_method, services::Method::Post | services::Method::Put) { + if matches!( + http_method, + services::Method::Post | services::Method::Put | services::Method::Patch + ) { headers.push(( "Digest".to_string(), format!("SHA-256={sha256}").into_masked(), @@ -231,9 +285,13 @@ impl api::PaymentAuthorize for Cybersource {} impl api::PaymentSync for Cybersource {} impl api::PaymentVoid for Cybersource {} impl api::PaymentCapture for Cybersource {} +impl api::PaymentIncrementalAuthorization for Cybersource {} impl api::MandateSetup for Cybersource {} impl api::ConnectorAccessToken for Cybersource {} impl api::PaymentToken for Cybersource {} +impl api::PaymentsPreProcessing for Cybersource {} +impl api::PaymentsCompleteAuthorize for Cybersource {} +impl api::ConnectorMandateRevoke for Cybersource {} impl ConnectorIntegration< @@ -252,8 +310,160 @@ 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 { + let connector_req = cybersource::CybersourceZeroMandateRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_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)?) + .set_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< + api::MandateRevoke, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + > for Cybersource +{ + fn get_headers( + &self, + req: &types::MandateRevokeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + fn get_http_method(&self) -> services::Method { + services::Method::Delete + } + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + fn get_url( + &self, + req: &types::MandateRevokeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}tms/v1/paymentinstruments/{}", + self.base_url(connectors), + connector_utils::RevokeMandateRequestData::get_connector_mandate_id(&req.request)? + )) + } + fn build_request( + &self, + req: &types::MandateRevokeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Delete) + .url(&types::MandateRevokeType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::MandateRevokeType::get_headers( + self, req, connectors, + )?) + .build(), + )) + } + fn handle_response( + &self, + data: &types::MandateRevokeRouterData, + res: types::Response, + ) -> CustomResult { + if matches!(res.status_code, 204) { + Ok(types::MandateRevokeRouterData { + response: Ok(types::MandateRevokeResponseData { + mandate_status: common_enums::MandateStatus::Revoked, + }), + ..data.clone() + }) + } else { + // If http_code != 204 || http_code != 4xx, we dont know any other response scenario yet. + let response_value: serde_json::Value = serde_json::from_slice(&res.response) + .into_report() + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + let response_string = response_value.to_string(); + + Ok(types::MandateRevokeRouterData { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: response_string.clone(), + reason: Some(response_string), + status_code: res.status_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..data.clone() + }) + } + } + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} impl ConnectorIntegration for Cybersource { @@ -267,6 +477,113 @@ impl ConnectorIntegration for Cybersource +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + 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::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let redirect_response = req.request.redirect_response.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "redirect_response", + }, + )?; + match redirect_response.params { + Some(param) if !param.clone().peek().is_empty() => Ok(format!( + "{}risk/v1/authentications", + self.base_url(connectors) + )), + Some(_) | None => Ok(format!( + "{}risk/v1/authentication-results", + self.base_url(connectors) + )), + } + } + fn get_request_body( + &self, + req: &types::PaymentsPreProcessingRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = cybersource::CybersourceRouterData::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_req = + cybersource::CybersourcePreProcessingRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsPreProcessingType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourcePreProcessingResponse = res + .response + .parse_struct("Cybersource AuthEnrollmentResponse") + .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 for Cybersource { @@ -299,14 +616,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = cybersource::CybersourcePaymentsRequest::try_from(req)?; - let cybersource_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(cybersource_payments_request)) + ) -> CustomResult { + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_req = + cybersource::CybersourcePaymentsCaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -321,7 +640,7 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourceServerErrorResponse = res + .response + .parse_struct("CybersourceServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration @@ -394,16 +730,6 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - Ok(Some( - types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - )) - } fn build_request( &self, req: &types::PaymentsSyncRouterData, @@ -427,17 +753,11 @@ impl ConnectorIntegration CustomResult { - Ok(format!( - "{}pts/v2/payments/", - api::ConnectorCommon::base_url(self, connectors) - )) + if req.is_three_ds() + && req.request.is_card() + && req.request.connector_mandate_id().is_none() + { + Ok(format!( + "{}risk/v1/authentication-setups", + api::ConnectorCommon::base_url(self, connectors) + )) + } else { + Ok(format!( + "{}pts/v2/payments/", + api::ConnectorCommon::base_url(self, connectors) + )) + } } fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = cybersource::CybersourceRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = - cybersource::CybersourcePaymentsRequest::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, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cybersource_payments_request)) + if req.is_three_ds() + && req.request.is_card() + && req.request.connector_mandate_id().is_none() + { + let connector_req = + cybersource::CybersourceAuthSetupRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } else { + let connector_req = + cybersource::CybersourcePaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } } fn build_request( @@ -508,7 +842,7 @@ impl ConnectorIntegration CustomResult { - let response: cybersource::CybersourcePaymentsResponse = res - .response - .parse_struct("Cybersource PaymentResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - let is_auto_capture = - data.request.capture_method == Some(diesel_models::enums::CaptureMethod::Automatic); - types::RouterData::try_from(( - types::ResponseRouterData { + if data.is_three_ds() + && data.request.is_card() + && data.request.connector_mandate_id().is_none() + { + let response: cybersource::CybersourceAuthSetupResponse = res + .response + .parse_struct("Cybersource AuthSetupResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, + }) + } else { + let response: cybersource::CybersourcePaymentsResponse = res + .response + .parse_struct("Cybersource 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: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } + + fn get_5xx_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourceServerErrorResponse = res + .response + .parse_struct("CybersourceServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let attempt_status = match response.reason { + Some(reason) => match reason { + transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure), + transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None, }, - is_auto_capture, + None => None, + }; + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status, + connector_transaction_id: None, + }) + } +} + +impl + ConnectorIntegration< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for Cybersource +{ + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + 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::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}pts/v2/payments/", + api::ConnectorCommon::base_url(self, connectors) )) - .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = + cybersource::CybersourcePaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + fn build_request( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCompleteAuthorizeRouterData, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourcePaymentsResponse = res + .response + .parse_struct("Cybersource 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( @@ -542,6 +1000,33 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourceServerErrorResponse = res + .response + .parse_struct("CybersourceServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let attempt_status = match response.reason { + Some(reason) => match reason { + transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure), + transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None, + }, + None => None, + }; + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration @@ -562,9 +1047,8 @@ impl ConnectorIntegration CustomResult { let connector_payment_id = req.request.connector_transaction_id.clone(); Ok(format!( - "{}pts/v2/payments/{}/voids", - self.base_url(connectors), - connector_payment_id + "{}pts/v2/payments/{connector_payment_id}/reversals", + self.base_url(connectors) )) } @@ -574,13 +1058,26 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - Ok(Some( - types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - )) + ) -> CustomResult { + let connector_router_data = cybersource::CybersourceRouterData::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_req = cybersource::CybersourceVoidRequest::try_from(&connector_router_data)?; + + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -594,7 +1091,7 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourceServerErrorResponse = res + .response + .parse_struct("CybersourceServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } } impl api::Refund for Cybersource {} @@ -664,14 +1178,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = cybersource::CybersourceRefundRequest::try_from(req)?; - let cybersource_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cybersource_refund_request)) + ) -> CustomResult { + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_req = + cybersource::CybersourceRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -686,7 +1202,7 @@ impl ConnectorIntegration CustomResult { - let response: cybersource::CybersourcePaymentsResponse = res + let response: cybersource::CybersourceRefundResponse = res .response - .parse_struct("Cybersource PaymentResponse") + .parse_struct("Cybersource RefundResponse") .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) } fn get_error_response( &self, @@ -755,9 +1270,6 @@ impl ConnectorIntegration CustomResult { - let response: cybersource::CybersourceTransactionResponse = res + let response: cybersource::CybersourceRsyncResponse = res .response - .parse_struct("Cybersource RefundsSyncResponse") + .parse_struct("Cybersource RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::ResponseRouterData { + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, - } - .try_into() + }) + } + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl + ConnectorIntegration< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for Cybersource +{ + fn get_headers( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_http_method(&self) -> services::Method { + services::Method::Patch + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.additional_amount, + req, + ))?; + let connector_request = + cybersource::CybersourcePaymentsIncrementalAuthorizationRequest::try_from( + &connector_router_data, + )?; + Ok(RequestContent::Json(Box::new(connector_request))) + } + fn build_request( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Patch) + .url(&types::IncrementalAuthorizationType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::IncrementalAuthorizationType::get_headers( + self, req, connectors, + )?) + .set_body(types::IncrementalAuthorizationType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + fn handle_response( + &self, + data: &types::PaymentsIncrementalAuthorizationRouterData, + res: types::Response, + ) -> CustomResult< + types::RouterData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + >, + errors::ConnectorError, + > { + let response: cybersource::CybersourcePaymentsIncrementalAuthorizationResponse = res + .response + .parse_struct("Cybersource PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(( + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + true, + )) .change_context(errors::ConnectorError::ResponseHandlingFailed) } fn get_error_response( @@ -805,7 +1423,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..324fe77d0bc5 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -1,17 +1,26 @@ use api_models::payments; -use common_utils::pii; -use masking::Secret; +use base64::Engine; +use common_utils::{ext_traits::ValueExt, pii}; +use error_stack::{IntoReport, ResultExt}; +use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use crate::{ - connector::utils::{self, AddressDetailsData, PhoneDetailsData, RouterData}, + connector::utils::{ + self, AddressDetailsData, ApplePayDecrypt, CardData, PaymentsAuthorizeRequestData, + PaymentsCompleteAuthorizeRequestData, PaymentsPreProcessingData, + PaymentsSetupMandateRequestData, PaymentsSyncRequestData, RecurringMandateData, RouterData, + }, consts, core::errors, - pii::PeekInterface, + services, types::{ self, api::{self, enums as api_enums}, storage::enums, + transformers::ForeignFrom, + ApplePayPredecryptData, }, }; @@ -38,6 +47,7 @@ impl T, ), ) -> Result { + // This conversion function is used at different places in the file, if updating this, keep a check for those let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; Ok(Self { amount, @@ -46,64 +56,403 @@ 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 email = item.request.get_email()?; + let bill_to = build_bill_to(item.get_billing()?, email)?; + + let order_information = OrderInformationWithBill { + amount_details: Amount { + total_amount: "0".to_string(), + currency: item.request.currency, + }, + bill_to: Some(bill_to), + }; + let (action_list, action_token_types, authorization_options) = ( + Some(vec![CybersourceActionsList::TokenCreate]), + Some(vec![CybersourceActionsTokenType::PaymentInstrument]), + Some(CybersourceAuthorizationOptions { + initiator: Some(CybersourcePaymentInitiator { + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, + }), + merchant_intitiated_transaction: None, + }), + ); + + let client_reference_information = ClientReferenceInformation { + code: Some(item.connector_request_reference_id.clone()), + }; + + let (payment_information, solution) = match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(ccard) => { + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + ( + 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, + }, + }), + None, + ) + } + + api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + payments::WalletData::ApplePay(apple_pay_data) => { + match item.payment_method_token.clone() { + Some(payment_method_token) => match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + let expiration_month = decrypt_data.get_expiry_month()?; + let expiration_year = decrypt_data.get_four_digit_expiry_year()?; + ( + PaymentInformation::ApplePay(ApplePayPaymentInformation { + tokenized_card: TokenizedCard { + number: decrypt_data.application_primary_account_number, + cryptogram: decrypt_data + .payment_data + .online_payment_cryptogram, + transaction_type: TransactionType::ApplePay, + expiration_year, + expiration_month, + }, + }), + Some(PaymentSolution::ApplePay), + ) + } + types::PaymentMethodToken::Token(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }, + None => ( + PaymentInformation::ApplePayToken(ApplePayTokenPaymentInformation { + fluid_data: FluidData { + value: Secret::from(apple_pay_data.payment_data), + }, + tokenized_card: ApplePayTokenizedCard { + transaction_type: TransactionType::ApplePay, + }, + }), + Some(PaymentSolution::ApplePay), + ), + } + } + payments::WalletData::GooglePay(google_pay_data) => ( + PaymentInformation::GooglePay(GooglePayPaymentInformation { + fluid_data: FluidData { + value: Secret::from( + consts::BASE64_ENGINE + .encode(google_pay_data.tokenization_data.token), + ), + }, + }), + Some(PaymentSolution::GooglePay), + ), + 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::WeChatPayQr(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ))?, + }, + 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("Cybersource"), + ))? + } + }; + + let processing_information = ProcessingInformation { + capture: Some(false), + capture_options: None, + action_list, + action_token_types, + authorization_options, + commerce_indicator: String::from("internet"), + payment_solution: solution.map(String::from), + }; + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentsRequest { processing_information: ProcessingInformation, payment_information: PaymentInformation, order_information: OrderInformationWithBill, client_reference_information: ClientReferenceInformation, + #[serde(skip_serializing_if = "Option::is_none")] + consumer_authentication_information: Option, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, } -#[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: String, + capture: Option, capture_options: Option, + payment_solution: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthInformation { + ucaf_collection_indicator: Option, + cavv: Option, + ucaf_authentication_data: Option, + xid: Option, + directory_server_transaction_id: Option, + specification_version: Option, +} +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantDefinedInformation { + key: u8, + value: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceActionsList { + TokenCreate, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourceActionsTokenType { + PaymentInstrument, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceAuthorizationOptions { + initiator: Option, + merchant_intitiated_transaction: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantInitiatedTransaction { + reason: Option, + //Required for recurring mandates payment + original_authorized_amount: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentInitiator { + #[serde(rename = "type")] + initiator_type: Option, + credential_stored_on_file: Option, + stored_credential_used: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourcePaymentInitiatorTypes { + Customer, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[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 PaymentInformation { +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CardPaymentInformation { card: Card, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenizedCard { + number: Secret, + expiration_month: Secret, + expiration_year: Secret, + cryptogram: Secret, + transaction_type: TransactionType, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayTokenizedCard { + transaction_type: TransactionType, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayTokenPaymentInformation { + fluid_data: FluidData, + tokenized_card: ApplePayTokenizedCard, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayPaymentInformation { + tokenized_card: TokenizedCard, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MandatePaymentInformation { + payment_instrument: CybersoucrePaymentInstrument, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FluidData { + value: Secret, +} + +#[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), + ApplePay(ApplePayPaymentInformation), + ApplePayToken(ApplePayTokenPaymentInformation), + MandatePayment(MandatePaymentInformation), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CybersoucrePaymentInstrument { + id: String, +} +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Card { number: cards::CardNumber, expiration_month: Secret, expiration_year: Secret, security_code: Secret, + #[serde(rename = "type")] + card_type: Option, } - -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformationWithBill { amount_details: Amount, - bill_to: BillTo, + bill_to: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformationIncrementalAuthorization { + amount_details: AdditionalAmount, } -#[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: api_models::enums::Currency, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdditionalAmount { + additional_amount: String, currency: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +pub enum PaymentSolution { + ApplePay, + GooglePay, +} + +#[derive(Debug, Serialize)] +pub enum TransactionType { + #[serde(rename = "1")] + ApplePay, +} + +impl From for String { + fn from(solution: PaymentSolution) -> Self { + let payment_solution = match solution { + PaymentSolution::ApplePay => "001", + PaymentSolution::GooglePay => "012", + }; + payment_solution.to_string() + } +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BillTo { first_name: Secret, @@ -114,154 +463,833 @@ pub struct BillTo { postal_code: Secret, country: api_enums::CountryAlpha2, email: pii::Email, - phone_number: Secret, +} + +impl From<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> + for ClientReferenceInformation +{ + fn from(item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + code: Some(item.router_data.connector_request_reference_id.clone()), + } + } +} + +impl From<&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for ClientReferenceInformation +{ + fn from(item: &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>) -> Self { + Self { + code: Some(item.router_data.connector_request_reference_id.clone()), + } + } +} + +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + )> for ProcessingInformation +{ + type Error = error_stack::Report; + fn try_from( + (item, solution): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + ), + ) -> Result { + let (action_list, action_token_types, authorization_options) = + if item.router_data.request.setup_mandate_details.is_some() { + ( + Some(vec![CybersourceActionsList::TokenCreate]), + Some(vec![CybersourceActionsTokenType::PaymentInstrument]), + Some(CybersourceAuthorizationOptions { + initiator: Some(CybersourcePaymentInitiator { + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, + }), + merchant_intitiated_transaction: None, + }), + ) + } else if item.router_data.request.connector_mandate_id().is_some() { + let original_amount = item + .router_data + .get_recurring_mandate_payment_data()? + .get_original_payment_amount()?; + let original_currency = item + .router_data + .get_recurring_mandate_payment_data()? + .get_original_payment_currency()?; + ( + None, + None, + Some(CybersourceAuthorizationOptions { + initiator: None, + merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { + reason: None, + original_authorized_amount: Some(utils::get_amount_as_string( + &types::api::CurrencyUnit::Base, + original_amount, + original_currency, + )?), + }), + }), + ) + } else { + (None, None, None) + }; + Ok(Self { + capture: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + payment_solution: solution.map(String::from), + action_list, + action_token_types, + authorization_options, + capture_options: None, + commerce_indicator: String::from("internet"), + }) + } +} + +impl + From<( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + Option, + &CybersourceConsumerAuthValidateResponse, + )> for ProcessingInformation +{ + fn from( + (item, solution, three_ds_data): ( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + Option, + &CybersourceConsumerAuthValidateResponse, + ), + ) -> Self { + 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::PaymentInstrument]), + Some(CybersourceAuthorizationOptions { + initiator: Some(CybersourcePaymentInitiator { + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, + }), + merchant_intitiated_transaction: None, + }), + ) + } else { + (None, None, None) + }; + Self { + capture: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + payment_solution: solution.map(String::from), + action_list, + action_token_types, + authorization_options, + capture_options: None, + commerce_indicator: three_ds_data + .indicator + .to_owned() + .unwrap_or(String::from("internet")), + } + } +} + +impl + From<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + BillTo, + )> for OrderInformationWithBill +{ + fn from( + (item, bill_to): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + BillTo, + ), + ) -> Self { + Self { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency, + }, + bill_to: Some(bill_to), + } + } +} + +impl + From<( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + BillTo, + )> for OrderInformationWithBill +{ + fn from( + (item, bill_to): ( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + BillTo, + ), + ) -> Self { + Self { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency, + }, + bill_to: Some(bill_to), + } + } } // for cybersource each item in Billing is mandatory fn build_bill_to( address_details: &payments::Address, email: pii::Email, - phone_number: Secret, ) -> Result> { let address = address_details .address .as_ref() .ok_or_else(utils::missing_field_err("billing.address"))?; + let mut state = address.to_state_code()?.peek().clone(); + state.truncate(20); 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()?, + administrative_area: Secret::from(state), postal_code: address.get_zip()?.to_owned(), country: address.get_country()?.to_owned(), email, - phone_number, }) } -impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> - for CybersourcePaymentsRequest +impl ForeignFrom for Vec { + fn foreign_from(metadata: Value) -> Self { + let hashmap: std::collections::BTreeMap = + serde_json::from_str(&metadata.to_string()) + .unwrap_or(std::collections::BTreeMap::new()); + let mut vector: Self = Self::new(); + let mut iter = 1; + for (key, value) in hashmap { + vector.push(MerchantDefinedInformation { + key: iter, + value: format!("{key}={value}"), + }); + iter += 1; + } + vector + } +} + +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + )> for CybersourcePaymentsRequest { type Error = error_stack::Report; fn try_from( - item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + (item, ccard): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + ), ) -> Result { - 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 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 { - card: Card { - number: ccard.card_number, - expiration_month: ccard.card_exp_month, - expiration_year: ccard.card_exp_year, - security_code: ccard.card_cvc, - }, - }; + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; - let processing_information = ProcessingInformation { - capture: matches!( - item.router_data.request.capture_method, - Some(enums::CaptureMethod::Automatic) | None - ), - capture_options: 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 client_reference_information = ClientReferenceInformation { - code: Some(item.router_data.connector_request_reference_id.clone()), - }; + let processing_information = ProcessingInformation::try_from((item, None))?; + let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); - Ok(Self { - processing_information, - payment_information, - order_information, - client_reference_information, - }) - } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), - } + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information: None, + merchant_defined_information, + }) } } -impl TryFrom<&types::PaymentsCaptureRouterData> for CybersourcePaymentsRequest { +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + payments::Card, + )> for CybersourcePaymentsRequest +{ type Error = error_stack::Report; - fn try_from(value: &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() - }, - client_reference_information: ClientReferenceInformation { - code: Some(value.connector_request_reference_id.clone()), + fn try_from( + (item, ccard): ( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + payments::Card, + ), + ) -> 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 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, }, - ..Default::default() + }); + let client_reference_information = ClientReferenceInformation::from(item); + + let three_ds_info: CybersourceThreeDSMetadata = item + .router_data + .request + .connector_meta + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "connector_meta", + })? + .parse_value("CybersourceThreeDSMetadata") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + + let processing_information = + ProcessingInformation::from((item, None, &three_ds_info.three_ds_data)); + + let consumer_authentication_information = Some(CybersourceConsumerAuthInformation { + ucaf_collection_indicator: three_ds_info.three_ds_data.ucaf_collection_indicator, + cavv: three_ds_info.three_ds_data.cavv, + ucaf_authentication_data: three_ds_info.three_ds_data.ucaf_authentication_data, + xid: three_ds_info.three_ds_data.xid, + directory_server_transaction_id: three_ds_info + .three_ds_data + .directory_server_transaction_id, + specification_version: three_ds_info.three_ds_data.specification_version, + }); + + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information, + merchant_defined_information, }) } } -impl TryFrom<&types::RefundExecuteRouterData> for CybersourcePaymentsRequest { +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + Box, + )> 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(), - }, - ..Default::default() - }, - client_reference_information: ClientReferenceInformation { - code: Some(value.connector_request_reference_id.clone()), + fn try_from( + (item, apple_pay_data): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + Box, + ), + ) -> 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 processing_information = + ProcessingInformation::try_from((item, Some(PaymentSolution::ApplePay)))?; + let client_reference_information = ClientReferenceInformation::from(item); + let expiration_month = apple_pay_data.get_expiry_month()?; + let expiration_year = apple_pay_data.get_four_digit_expiry_year()?; + let payment_information = PaymentInformation::ApplePay(ApplePayPaymentInformation { + tokenized_card: TokenizedCard { + number: apple_pay_data.application_primary_account_number, + cryptogram: apple_pay_data.payment_data.online_payment_cryptogram, + transaction_type: TransactionType::ApplePay, + expiration_year, + expiration_month, }, - ..Default::default() + }); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information: None, + merchant_defined_information, }) } } -pub struct CybersourceAuthType { - pub(super) api_key: Secret, - pub(super) merchant_account: Secret, - pub(super) api_secret: Secret, -} - -impl TryFrom<&types::ConnectorAuthType> for CybersourceAuthType { +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + payments::GooglePayWalletData, + )> for CybersourcePaymentsRequest +{ type Error = error_stack::Report; - fn try_from(auth_type: &types::ConnectorAuthType) -> Result { - if let types::ConnectorAuthType::SignatureKey { - api_key, - key1, - api_secret, + fn try_from( + (item, google_pay_data): ( + &CybersourceRouterData<&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::try_from((item, Some(PaymentSolution::GooglePay)))?; + let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information: None, + merchant_defined_information, + }) + } +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> + for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.connector_mandate_id() { + Some(connector_mandate_id) => Self::try_from((item, connector_mandate_id)), + None => { + 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::ApplePay(apple_pay_data) => { + match item.router_data.payment_method_token.clone() { + Some(payment_method_token) => match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + Self::try_from((item, decrypt_data)) + } + types::PaymentMethodToken::Token(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }, + None => { + 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 processing_information = ProcessingInformation::try_from( + (item, Some(PaymentSolution::ApplePay)), + )?; + let client_reference_information = + ClientReferenceInformation::from(item); + let payment_information = PaymentInformation::ApplePayToken( + ApplePayTokenPaymentInformation { + fluid_data: FluidData { + value: Secret::from(apple_pay_data.payment_data), + }, + tokenized_card: ApplePayTokenizedCard { + transaction_type: TransactionType::ApplePay, + }, + }, + ); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from( + metadata.peek().to_owned(), + ) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + consumer_authentication_information: None, + }) + } + } + } + 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::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( + "Cybersource", + ), + ) + .into()) + } + }, + // If connector_mandate_id is present MandatePayment will be the PMD, the case will be handled in the first `if` clause. + // This is a fallback implementation in the event of catastrophe. + payments::PaymentMethodData::MandatePayment => { + let connector_mandate_id = + item.router_data.request.connector_mandate_id().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "connector_mandate_id", + }, + )?; + Self::try_from((item, connector_mandate_id)) + } + payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | 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"), + ) + .into()) + } + } + } + } + } +} + +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + String, + )> for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, connector_mandate_id): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + String, + ), + ) -> Result { + let processing_information = ProcessingInformation::try_from((item, None))?; + let payment_instrument = CybersoucrePaymentInstrument { + id: connector_mandate_id, + }; + 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::MandatePayment(MandatePaymentInformation { payment_instrument }); + let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + consumer_authentication_information: None, + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceAuthSetupRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> + for CybersourceAuthSetupRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + payments::PaymentMethodData::Card(ccard) => { + 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 client_reference_information = ClientReferenceInformation::from(item); + Ok(Self { + payment_information, + client_reference_information, + }) + } + payments::PaymentMethodData::Wallet(_) + | 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("Cybersource"), + ) + .into()) + } + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsCaptureRequest { + processing_information: ProcessingInformation, + order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsIncrementalAuthorizationRequest { + processing_information: ProcessingInformation, + order_information: OrderInformationIncrementalAuthorization, +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> + for CybersourcePaymentsCaptureRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + Ok(Self { + processing_information: ProcessingInformation { + capture_options: Some(CaptureOptions { + capture_sequence_number: 1, + total_capture_count: 1, + }), + action_list: None, + action_token_types: None, + authorization_options: None, + capture: None, + commerce_indicator: String::from("internet"), + payment_solution: None, + }, + order_information: OrderInformationWithBill { + amount_details: Amount { + total_amount: item.amount.clone(), + currency: item.router_data.request.currency, + }, + bill_to: None, + }, + client_reference_information: ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }, + merchant_defined_information, + }) + } +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRouterData>> + for CybersourcePaymentsIncrementalAuthorizationRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRouterData>, + ) -> Result { + Ok(Self { + processing_information: ProcessingInformation { + action_list: None, + action_token_types: None, + authorization_options: Some(CybersourceAuthorizationOptions { + initiator: Some(CybersourcePaymentInitiator { + initiator_type: None, + credential_stored_on_file: None, + stored_credential_used: Some(true), + }), + merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { + reason: Some("5".to_owned()), + original_authorized_amount: None, + }), + }), + commerce_indicator: String::from("internet"), + capture: None, + capture_options: None, + payment_solution: None, + }, + order_information: OrderInformationIncrementalAuthorization { + amount_details: AdditionalAmount { + additional_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), + }, + }, + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceVoidRequest { + client_reference_information: ClientReferenceInformation, + reversal_information: ReversalInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, + // The connector documentation does not mention the merchantDefinedInformation field for Void requests. But this has been still added because it works! +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReversalInformation { + amount_details: Amount, + reason: String, +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsCancelRouterData>> for CybersourceVoidRequest { + type Error = error_stack::Report; + fn try_from( + value: &CybersourceRouterData<&types::PaymentsCancelRouterData>, + ) -> Result { + let merchant_defined_information = + value.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + Ok(Self { + 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", + })?, + }, + merchant_defined_information, + }) + } +} + +pub struct CybersourceAuthType { + pub(super) api_key: Secret, + pub(super) merchant_account: Secret, + pub(super) api_secret: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for CybersourceAuthType { + 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 { @@ -274,7 +1302,8 @@ impl TryFrom<&types::ConnectorAuthType> for CybersourceAuthType { } } } -#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq)] + +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CybersourcePaymentStatus { Authorized, @@ -284,64 +1313,988 @@ pub enum CybersourcePaymentStatus { Reversed, Pending, Declined, + Rejected, + Challenge, AuthorizedPendingReview, + AuthorizedRiskDeclined, Transmitted, - #[default] - Processing, + InvalidRequest, + ServerError, + PendingAuthentication, + PendingReview, + //PartialAuthorized, not being consumed yet. } -impl From for enums::AttemptStatus { - fn from(item: CybersourcePaymentStatus) -> Self { - match item { +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceIncrementalAuthorizationStatus { + Authorized, + Declined, + AuthorizedPendingReview, +} + +impl ForeignFrom<(CybersourcePaymentStatus, bool)> for enums::AttemptStatus { + fn foreign_from((status, capture): (CybersourcePaymentStatus, bool)) -> Self { + match status { CybersourcePaymentStatus::Authorized - | CybersourcePaymentStatus::AuthorizedPendingReview => Self::Authorized, + | CybersourcePaymentStatus::AuthorizedPendingReview => { + if capture { + // Because Cybersource will return Payment Status as Authorized even in AutoCapture Payment + Self::Charged + } else { + Self::Authorized + } + } + CybersourcePaymentStatus::Pending => { + if capture { + Self::Charged + } else { + Self::Pending + } + } CybersourcePaymentStatus::Succeeded | CybersourcePaymentStatus::Transmitted => { Self::Charged } - CybersourcePaymentStatus::Voided | CybersourcePaymentStatus::Reversed => Self::Voided, - CybersourcePaymentStatus::Failed | CybersourcePaymentStatus::Declined => Self::Failure, - CybersourcePaymentStatus::Processing => Self::Authorizing, - CybersourcePaymentStatus::Pending => Self::Pending, + CybersourcePaymentStatus::Voided | CybersourcePaymentStatus::Reversed => Self::Voided, + CybersourcePaymentStatus::Failed + | CybersourcePaymentStatus::Declined + | CybersourcePaymentStatus::AuthorizedRiskDeclined + | CybersourcePaymentStatus::Rejected + | CybersourcePaymentStatus::InvalidRequest + | CybersourcePaymentStatus::ServerError => Self::Failure, + CybersourcePaymentStatus::PendingAuthentication => Self::AuthenticationPending, + CybersourcePaymentStatus::PendingReview | CybersourcePaymentStatus::Challenge => { + Self::Pending + } + } + } +} + +impl From for common_enums::AuthorizationStatus { + fn from(item: CybersourceIncrementalAuthorizationStatus) -> Self { + match item { + CybersourceIncrementalAuthorizationStatus::Authorized + | CybersourceIncrementalAuthorizationStatus::AuthorizedPendingReview => Self::Success, + CybersourceIncrementalAuthorizationStatus::Declined => Self::Failure, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourcePaymentsResponse { + ClientReferenceInformation(CybersourceClientReferenceResponse), + ErrorInformation(CybersourceErrorInformationResponse), +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceClientReferenceResponse { + id: String, + status: CybersourcePaymentStatus, + client_reference_information: ClientReferenceInformation, + processor_information: Option, + risk_information: Option, + token_information: Option, + error_information: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceErrorInformationResponse { + id: String, + error_information: CybersourceErrorInformation, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthInformationResponse { + access_token: String, + device_data_collection_url: String, + reference_id: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientAuthSetupInfoResponse { + id: String, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: CybersourceConsumerAuthInformationResponse, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourceAuthSetupResponse { + ClientAuthSetupInfo(ClientAuthSetupInfoResponse), + ErrorInformation(CybersourceErrorInformationResponse), +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsIncrementalAuthorizationResponse { + status: CybersourceIncrementalAuthorizationStatus, + error_information: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourceSetupMandatesResponse { + ClientReferenceInformation(CybersourceClientReferenceResponse), + ErrorInformation(CybersourceErrorInformationResponse), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientReferenceInformation { + code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientProcessorInformation { + avs: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Avs { + code: String, + code_raw: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientRiskInformation { + rules: Option>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ClientRiskInformationRules { + name: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceTokenInformation { + payment_instrument: CybersoucrePaymentInstrument, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CybersourceErrorInformation { + reason: Option, + message: Option, +} + +impl + From<( + &CybersourceErrorInformationResponse, + types::ResponseRouterData, + Option, + )> for types::RouterData +{ + fn from( + (error_response, item, transaction_status): ( + &CybersourceErrorInformationResponse, + types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + T, + types::PaymentsResponseData, + >, + Option, + ), + ) -> Self { + let error_reason = error_response + .error_information + .message + .to_owned() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason.to_owned(); + let response = Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }); + match transaction_status { + Some(status) => Self { + response, + status, + ..item.data + }, + None => Self { + response, + ..item.data + }, + } + } +} + +fn get_error_response_if_failure( + (info_response, status, http_code): ( + &CybersourceClientReferenceResponse, + enums::AttemptStatus, + u16, + ), +) -> Option { + if utils::is_payment_failure(status) { + Some(types::ErrorResponse::from(( + &info_response.error_information, + &info_response.risk_information, + http_code, + info_response.id.clone(), + ))) + } else { + None + } +} + +fn get_payment_response( + (info_response, status, http_code): ( + &CybersourceClientReferenceResponse, + enums::AttemptStatus, + u16, + ), +) -> Result { + let error_response = get_error_response_if_failure((info_response, status, http_code)); + match error_response { + Some(error) => Err(error), + None => { + let incremental_authorization_allowed = + Some(status == enums::AttemptStatus::Authorized); + let mandate_reference = + info_response + .token_information + .clone() + .map(|token_info| types::MandateReference { + connector_mandate_id: Some(token_info.payment_instrument.id), + payment_method_id: None, + }); + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), + redirection_data: None, + mandate_reference, + connector_metadata: info_response + .processor_information + .as_ref() + .map(|processor_information| serde_json::json!({"avs_response": processor_information.avs})), + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .clone() + .unwrap_or(info_response.id.clone()), + ), + incremental_authorization_allowed, + }) + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { + let status = enums::AttemptStatus::foreign_from(( + info_response.status.clone(), + item.data.request.is_auto_capture()?, + )); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } + CybersourcePaymentsResponse::ErrorInformation(ref error_response) => Ok(Self::from(( + &error_response.clone(), + item, + Some(enums::AttemptStatus::Failure), + ))), + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourceAuthSetupResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourceAuthSetupResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourceAuthSetupResponse::ClientAuthSetupInfo(info_response) => Ok(Self { + status: enums::AttemptStatus::AuthenticationPending, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: Some(services::RedirectForm::CybersourceAuthSetup { + access_token: info_response + .consumer_authentication_information + .access_token, + ddc_url: info_response + .consumer_authentication_information + .device_data_collection_url, + reference_id: info_response + .consumer_authentication_information + .reference_id, + }), + 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.clone()), + ), + incremental_authorization_allowed: None, + }), + ..item.data + }), + CybersourceAuthSetupResponse::ErrorInformation(error_response) => { + let error_reason = error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason; + Ok(Self { + response: Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }), + status: enums::AttemptStatus::AuthenticationFailed, + ..item.data + }) + } + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthInformationRequest { + return_url: String, + reference_id: String, +} +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceAuthEnrollmentRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: CybersourceConsumerAuthInformationRequest, + order_information: OrderInformationWithBill, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct CybersourceRedirectionAuthResponse { + pub transaction_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthInformationValidateRequest { + authentication_transaction_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceAuthValidateRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: CybersourceConsumerAuthInformationValidateRequest, + order_information: OrderInformation, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum CybersourcePreProcessingRequest { + AuthEnrollment(CybersourceAuthEnrollmentRequest), + AuthValidate(CybersourceAuthValidateRequest), +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsPreProcessingRouterData>> + for CybersourcePreProcessingRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsPreProcessingRouterData>, + ) -> Result { + let client_reference_information = ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }; + let payment_method_data = item.router_data.request.payment_method_data.clone().ok_or( + errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "payment_method_data", + }, + )?; + let payment_information = match payment_method_data { + payments::PaymentMethodData::Card(ccard) => { + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + Ok(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, + }, + })) + } + payments::PaymentMethodData::Wallet(_) + | 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("Cybersource"), + )) + } + }?; + + let redirect_response = item.router_data.request.redirect_response.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "redirect_response", + }, + )?; + + let amount_details = Amount { + total_amount: item.amount.clone(), + currency: item.router_data.request.currency.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "currency", + }, + )?, + }; + + match redirect_response.params { + Some(param) if !param.clone().peek().is_empty() => { + let reference_id = param + .clone() + .peek() + .split_once('=') + .ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "request.redirect_response.params.reference_id", + })? + .1 + .to_string(); + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill { + amount_details, + bill_to: Some(bill_to), + }; + Ok(Self::AuthEnrollment(CybersourceAuthEnrollmentRequest { + payment_information, + client_reference_information, + consumer_authentication_information: + CybersourceConsumerAuthInformationRequest { + return_url: item.router_data.request.get_complete_authorize_url()?, + reference_id, + }, + order_information, + })) + } + Some(_) | None => { + let redirect_payload: CybersourceRedirectionAuthResponse = redirect_response + .payload + .ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "request.redirect_response.payload", + })? + .peek() + .clone() + .parse_value("CybersourceRedirectionAuthResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let order_information = OrderInformation { amount_details }; + Ok(Self::AuthValidate(CybersourceAuthValidateRequest { + payment_information, + client_reference_information, + consumer_authentication_information: + CybersourceConsumerAuthInformationValidateRequest { + authentication_transaction_id: redirect_payload.transaction_id, + }, + order_information, + })) + } + } + } +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + ) -> Result { + let payment_method_data = item.router_data.request.payment_method_data.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "payment_method_data", + }, + )?; + match payment_method_data { + payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + payments::PaymentMethodData::Wallet(_) + | 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("Cybersource"), + ) + .into()) + } + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceAuthEnrollmentStatus { + PendingAuthentication, + AuthenticationSuccessful, + AuthenticationFailed, +} +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthValidateResponse { + ucaf_collection_indicator: Option, + cavv: Option, + ucaf_authentication_data: Option, + xid: Option, + specification_version: Option, + directory_server_transaction_id: Option, + indicator: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CybersourceThreeDSMetadata { + three_ds_data: CybersourceConsumerAuthValidateResponse, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthInformationEnrollmentResponse { + access_token: Option, + step_up_url: Option, + //Added to segregate the three_ds_data in a separate struct + #[serde(flatten)] + validate_response: CybersourceConsumerAuthValidateResponse, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientAuthCheckInfoResponse { + id: String, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: CybersourceConsumerAuthInformationEnrollmentResponse, + status: CybersourceAuthEnrollmentStatus, + error_information: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourcePreProcessingResponse { + ClientAuthCheckInfo(Box), + ErrorInformation(CybersourceErrorInformationResponse), +} + +impl From for enums::AttemptStatus { + fn from(item: CybersourceAuthEnrollmentStatus) -> Self { + match item { + CybersourceAuthEnrollmentStatus::PendingAuthentication => Self::AuthenticationPending, + CybersourceAuthEnrollmentStatus::AuthenticationSuccessful => { + Self::AuthenticationSuccessful + } + CybersourceAuthEnrollmentStatus::AuthenticationFailed => Self::AuthenticationFailed, + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourcePreProcessingResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourcePreProcessingResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourcePreProcessingResponse::ClientAuthCheckInfo(info_response) => { + let status = enums::AttemptStatus::from(info_response.status); + let risk_info: Option = None; + if utils::is_payment_failure(status) { + let response = Err(types::ErrorResponse::from(( + &info_response.error_information, + &risk_info, + item.http_code, + info_response.id.clone(), + ))); + + Ok(Self { + status, + response, + ..item.data + }) + } else { + let connector_response_reference_id = Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id.clone()), + ); + + let redirection_data = match ( + info_response + .consumer_authentication_information + .access_token, + info_response + .consumer_authentication_information + .step_up_url, + ) { + (Some(access_token), Some(step_up_url)) => { + Some(services::RedirectForm::CybersourceConsumerAuth { + access_token, + step_up_url, + }) + } + _ => None, + }; + let three_ds_data = serde_json::to_value( + info_response + .consumer_authentication_information + .validate_response, + ) + .into_report() + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data, + mandate_reference: None, + connector_metadata: Some(serde_json::json!({ + "three_ds_data": three_ds_data + })), + network_txn_id: None, + connector_response_reference_id, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } + } + CybersourcePreProcessingResponse::ErrorInformation(ref error_response) => { + let error_reason = error_response + .error_information + .message + .to_owned() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason.to_owned(); + let response = Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }); + Ok(Self { + response, + status: enums::AttemptStatus::AuthenticationFailed, + ..item.data + }) + } + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { + let status = enums::AttemptStatus::foreign_from(( + info_response.status.clone(), + item.data.request.is_auto_capture()?, + )); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } + CybersourcePaymentsResponse::ErrorInformation(ref error_response) => Ok(Self::from(( + &error_response.clone(), + item, + Some(enums::AttemptStatus::Failure), + ))), + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { + let status = + enums::AttemptStatus::foreign_from((info_response.status.clone(), true)); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } + CybersourcePaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item, None))) + } } } } -impl From for enums::RefundStatus { - fn from(item: CybersourcePaymentStatus) -> Self { - match item { - CybersourcePaymentStatus::Succeeded | CybersourcePaymentStatus::Transmitted => { - Self::Success +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsCancelData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsCancelData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { + let status = + enums::AttemptStatus::foreign_from((info_response.status.clone(), false)); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } + CybersourcePaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item, None))) } - CybersourcePaymentStatus::Failed => Self::Failure, - _ => Self::Pending, } } } -#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct CybersourcePaymentsResponse { - id: String, - status: CybersourcePaymentStatus, - error_information: Option, - client_reference_information: Option, -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ClientReferenceInformation { - code: Option, -} +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 { + match item.response { + CybersourceSetupMandatesResponse::ClientReferenceInformation(info_response) => { + let mandate_reference = info_response.token_information.clone().map(|token_info| { + types::MandateReference { + connector_mandate_id: Some(token_info.payment_instrument.id), + payment_method_id: None, + } + }); + let mut mandate_status = + enums::AttemptStatus::foreign_from((info_response.status.clone(), false)); + 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 + } + let error_response = + get_error_response_if_failure((&info_response, mandate_status, item.http_code)); -#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] -pub struct CybersourceErrorInformation { - reason: String, - message: String, + Ok(Self { + status: mandate_status, + response: match error_response { + Some(error) => Err(error), + None => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + info_response.id.clone(), + ), + redirection_data: None, + mandate_reference, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .clone() + .unwrap_or(info_response.id), + ), + incremental_authorization_allowed: Some( + mandate_status == enums::AttemptStatus::Authorized, + ), + }), + }, + ..item.data + }) + } + CybersourceSetupMandatesResponse::ErrorInformation(ref error_response) => { + let error_reason = error_response + .error_information + .message + .to_owned() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason.to_owned(); + let response = Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }); + Ok(Self { + response, + status: enums::AttemptStatus::Failure, + ..item.data + }) + } + } + } } impl TryFrom<( - types::ResponseRouterData, + types::ResponseRouterData< + F, + CybersourcePaymentsIncrementalAuthorizationResponse, + T, + types::PaymentsResponseData, + >, bool, )> for types::RouterData { @@ -350,7 +2303,7 @@ impl data: ( types::ResponseRouterData< F, - CybersourcePaymentsResponse, + CybersourcePaymentsIncrementalAuthorizationResponse, T, types::PaymentsResponseData, >, @@ -358,193 +2311,222 @@ impl ), ) -> Result { let item = data.0; - let is_capture = data.1; Ok(Self { - status: get_payment_status(is_capture, item.response.status.into()), 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, - }), - _ => Ok(types::PaymentsResponseData::TransactionResponse { - 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: item - .response - .client_reference_information - .map(|cref| cref.code) - .unwrap_or(Some(item.response.id)), - }), + Some(error) => Ok( + types::PaymentsResponseData::IncrementalAuthorizationResponse { + status: common_enums::AuthorizationStatus::Failure, + error_code: error.reason, + error_message: error.message, + connector_authorization_id: None, + }, + ), + _ => Ok( + types::PaymentsResponseData::IncrementalAuthorizationResponse { + status: item.response.status.into(), + error_code: None, + error_message: None, + connector_authorization_id: None, + }, + ), }, ..item.data }) } } +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourceTransactionResponse { + ApplicationInformation(CybersourceApplicationInfoResponse), + ErrorInformation(CybersourceErrorInformationResponse), +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CybersourceTransactionResponse { +pub struct CybersourceApplicationInfoResponse { id: String, application_information: ApplicationInformation, client_reference_information: Option, + error_information: Option, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApplicationInformation { status: CybersourcePaymentStatus, } -fn get_payment_status(is_capture: bool, status: enums::AttemptStatus) -> enums::AttemptStatus { - let is_authorized = matches!(status, enums::AttemptStatus::Authorized); - if is_capture && is_authorized { - return enums::AttemptStatus::Pending; - } - status -} - -impl - TryFrom<( +impl + TryFrom< types::ResponseRouterData< F, CybersourceTransactionResponse, - T, + types::PaymentsSyncData, types::PaymentsResponseData, >, - bool, - )> for types::RouterData + > for types::RouterData { type Error = error_stack::Report; fn try_from( - data: ( - types::ResponseRouterData< - F, - CybersourceTransactionResponse, - T, - types::PaymentsResponseData, - >, - bool, - ), + item: types::ResponseRouterData< + F, + CybersourceTransactionResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, ) -> Result { - let item = data.0; - let is_capture = data.1; - Ok(Self { - status: get_payment_status( - is_capture, - item.response.application_information.status.into(), - ), - response: Ok(types::PaymentsResponseData::TransactionResponse { - 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: item - .response - .client_reference_information - .map(|cref| cref.code) - .unwrap_or(Some(item.response.id)), + match item.response { + CybersourceTransactionResponse::ApplicationInformation(app_response) => { + let status = enums::AttemptStatus::foreign_from(( + app_response.application_information.status, + item.data.request.is_auto_capture()?, + )); + let incremental_authorization_allowed = + Some(status == enums::AttemptStatus::Authorized); + let risk_info: Option = None; + if utils::is_payment_failure(status) { + Ok(Self { + response: Err(types::ErrorResponse::from(( + &app_response.error_information, + &risk_info, + item.http_code, + app_response.id.clone(), + ))), + status: enums::AttemptStatus::Failure, + ..item.data + }) + } else { + Ok(Self { + status, + 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, + }), + ..item.data + }) + } + } + CybersourceTransactionResponse::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 }), - ..item.data - }) + } } } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ErrorResponse { - 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: String, -} - -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceRefundRequest { order_information: OrderInformation, + client_reference_information: ClientReferenceInformation, } -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, }, }, + client_reference_information: ClientReferenceInformation { + code: Some(item.router_data.request.refund_id.clone()), + }, }) } } -impl TryFrom> +impl From for enums::RefundStatus { + fn from(item: CybersourceRefundStatus) -> Self { + match item { + CybersourceRefundStatus::Succeeded | CybersourceRefundStatus::Transmitted => { + Self::Success + } + CybersourceRefundStatus::Failed | CybersourceRefundStatus::Voided => Self::Failure, + CybersourceRefundStatus::Pending => Self::Pending, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceRefundStatus { + Succeeded, + Transmitted, + Failed, + Pending, + Voided, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceRefundResponse { + id: String, + status: CybersourceRefundStatus, +} + +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { - let refund_status = enums::RefundStatus::from(item.response.status); Ok(Self { response: Ok(types::RefundsResponseData { connector_refund_id: item.response.id, - refund_status, + refund_status: enums::RefundStatus::from(item.response.status), }), ..item.data }) } } -impl TryFrom> +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RsyncApplicationInformation { + status: CybersourceRefundStatus, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceRsyncResponse { + 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 { @@ -557,3 +2539,129 @@ impl TryFrom, + pub status: Option, + pub message: Option, + pub reason: Option, + pub details: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceNotAvailableErrorResponse { + pub errors: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceNotAvailableErrorObject { + #[serde(rename = "type")] + pub error_type: Option, + pub message: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceServerErrorResponse { + pub status: Option, + pub message: Option, + pub reason: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Reason { + SystemError, + ServerTimeout, + ServiceTimeout, +} + +#[derive(Debug, Deserialize)] +pub struct CybersourceAuthenticationErrorResponse { + pub response: AuthenticationErrorInformation, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourceErrorResponse { + AuthenticationError(CybersourceAuthenticationErrorResponse), + //If the request resource is not available/exists in cybersource + NotAvailableError(CybersourceNotAvailableErrorResponse), + StandardError(CybersourceStandardErrorResponse), +} + +#[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: String, +} + +#[derive(Debug, Default, Deserialize)] +pub struct AuthenticationErrorInformation { + pub rmsg: String, +} + +impl + From<( + &Option, + &Option, + u16, + String, + )> for types::ErrorResponse +{ + fn from( + (error_data, risk_information, status_code, transaction_id): ( + &Option, + &Option, + u16, + String, + ), + ) -> Self { + let avs_message = risk_information + .clone() + .map(|client_risk_information| { + client_risk_information.rules.map(|rules| { + rules + .iter() + .map(|risk_info| format!(" , {}", risk_info.name)) + .collect::>() + .join("") + }) + }) + .unwrap_or(Some("".to_string())); + let error_reason = error_data + .clone() + .map(|error_details| { + error_details.message.unwrap_or("".to_string()) + + &avs_message.unwrap_or("".to_string()) + }) + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_data + .clone() + .and_then(|error_details| error_details.reason); + + Self { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message + .clone() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason.clone()), + status_code, + attempt_status: Some(enums::AttemptStatus::Failure), + connector_transaction_id: Some(transaction_id.clone()), + } + } +} diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index 64d3e6f1c12f..1e6fc7561ed4 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -5,6 +5,7 @@ use std::fmt::Debug; use common_utils::{ crypto::{self, SignMessage}, date_time, + request::RequestContent, }; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; @@ -27,7 +28,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -56,16 +57,11 @@ where connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let dlocal_req = match self.get_request_body(req, connectors)? { - Some(val) => val, - None => types::RequestBody::log_and_get_request_body("".to_string(), Ok) - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - }; + let dlocal_req = self.get_request_body(req, connectors)?; let date = date_time::date_as_yyyymmddthhmmssmmmz() .into_report() .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let auth = dlocal::DlocalAuthType::try_from(&req.connector_auth_type)?; let sign_req: String = format!( "{}{}{}", @@ -136,6 +132,7 @@ impl ConnectorCommon for Dlocal { message: response.message, reason: response.param, attempt_status: None, + connector_transaction_id: None, }) } } @@ -183,6 +180,20 @@ impl types::PaymentsResponseData, > for Dlocal { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Dlocal".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -212,20 +223,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = dlocal::DlocalRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = dlocal::DlocalPaymentsRequest::try_from(&connector_router_data)?; - let dlocal_payments_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(dlocal_payments_request)) + let connector_req = dlocal::DlocalPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -243,7 +249,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = dlocal::DlocalPaymentsCaptureRequest::try_from(req)?; - let dlocal_payments_capture_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(dlocal_payments_capture_request)) + ) -> CustomResult { + let connector_req = dlocal::DlocalPaymentsCaptureRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -396,7 +397,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = dlocal::DlocalRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let connector_request = dlocal::DlocalRefundRequest::try_from(&connector_router_data)?; - let dlocal_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(dlocal_refund_request)) + let connector_req = dlocal::DlocalRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -553,7 +549,7 @@ impl ConnectorIntegration, - ) -> 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..5eb682d4cfa4 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -125,7 +125,10 @@ impl TryFrom<&DlocalRouterData<&types::PaymentsAuthorizeRouterData>> for DlocalP document: get_doc_from_currency(country.to_string()), }, card: Some(Card { - holder_name: ccard.card_holder_name.clone(), + holder_name: ccard + .card_holder_name + .clone() + .unwrap_or(Secret::new("".to_string())), number: ccard.card_number.clone(), cvv: ccard.card_cvc.clone(), expiration_month: ccard.card_exp_month.clone(), @@ -168,7 +171,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 +306,7 @@ pub struct DlocalPaymentsResponse { status: DlocalPaymentStatus, id: String, three_dsecure: Option, - order_id: String, + order_id: Option, } impl @@ -322,12 +326,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 +346,7 @@ impl pub struct DlocalPaymentsSyncResponse { status: DlocalPaymentStatus, id: String, - order_id: String, + order_id: Option, } impl @@ -361,14 +366,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 +383,7 @@ impl pub struct DlocalPaymentsCaptureResponse { status: DlocalPaymentStatus, id: String, - order_id: String, + order_id: Option, } impl @@ -399,14 +403,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 +446,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..28d9eaedf076 100644 --- a/crates/router/src/connector/dummyconnector.rs +++ b/crates/router/src/connector/dummyconnector.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; @@ -21,7 +22,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -112,6 +113,7 @@ impl ConnectorCommon for DummyConnector { message: response.error.message, reason: response.error.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -151,6 +153,20 @@ impl types::PaymentsResponseData, > for DummyConnector { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for DummyConnector".to_string(), + ) + .into()) + } } impl @@ -189,14 +205,9 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = transformers::DummyConnectorPaymentsRequest::::try_from(req)?; - let dummmy_payments_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::>::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(dummmy_payments_request)) + ) -> CustomResult { + let connector_req = transformers::DummyConnectorPaymentsRequest::::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -214,7 +225,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -352,7 +363,7 @@ impl &self, _req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -435,14 +446,9 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = transformers::DummyConnectorRefundRequest::try_from(req)?; - let dummmy_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(dummmy_refund_request)) + ) -> CustomResult { + let connector_req = transformers::DummyConnectorRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -457,7 +463,7 @@ impl ConnectorIntegration ConnectorIntegration 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..5c20d353583e 100644 --- a/crates/router/src/connector/dummyconnector/transformers.rs +++ b/crates/router/src/connector/dummyconnector/transformers.rs @@ -83,15 +83,18 @@ pub struct DummyConnectorCard { cvc: Secret, } -impl From for DummyConnectorCard { - fn from(value: api_models::payments::Card) -> Self { - Self { - name: value.card_holder_name, +impl TryFrom for DummyConnectorCard { + type Error = error_stack::Report; + fn try_from(value: api_models::payments::Card) -> Result { + Ok(Self { + name: value + .card_holder_name + .unwrap_or(Secret::new("".to_string())), number: value.card_number, expiry_month: value.card_exp_month, expiry_year: value.card_exp_year, cvc: value.card_cvc, - } + }) } } @@ -151,7 +154,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> .payment_method_data { api::PaymentMethodData::Card(ref req_card) => { - Ok(PaymentMethodData::Card(req_card.clone().into())) + Ok(PaymentMethodData::Card(req_card.clone().try_into()?)) } api::PaymentMethodData::Wallet(ref wallet_data) => { Ok(PaymentMethodData::Wallet(wallet_data.clone().try_into()?)) @@ -250,6 +253,7 @@ impl TryFrom for Fiserv { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Fiserv".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Fiserv {} @@ -247,14 +262,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservCancelRequest::try_from(req)?; - let fiserv_payments_cancel_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_payments_cancel_request)) + ) -> CustomResult { + let connector_req = fiserv::FiservCancelRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -268,7 +278,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservSyncRequest::try_from(req)?; - let fiserv_payments_sync_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_payments_sync_request)) + ) -> CustomResult { + let connector_req = fiserv::FiservSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -357,7 +362,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = fiserv::FiservRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let connector_request = fiserv::FiservCaptureRequest::try_from(&router_obj)?; - let fiserv_payments_capture_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_payments_capture_request)) + let connector_req = fiserv::FiservCaptureRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -441,7 +441,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = fiserv::FiservRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = fiserv::FiservPaymentsRequest::try_from(&router_obj)?; - let fiserv_payments_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_payments_request)) + let connector_req = fiserv::FiservPaymentsRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -557,7 +552,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = fiserv::FiservRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let connector_request = fiserv::FiservRefundRequest::try_from(&router_obj)?; - let fiserv_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_refund_request)) + let connector_req = fiserv::FiservRefundRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -649,7 +639,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservSyncRequest::try_from(req)?; - let fiserv_sync_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_sync_request)) + ) -> CustomResult { + let connector_req = fiserv::FiservSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -732,7 +717,7 @@ impl ConnectorIntegration, - ) -> 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..f23907232c48 100644 --- a/crates/router/src/connector/forte.rs +++ b/crates/router/src/connector/forte.rs @@ -3,6 +3,7 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; @@ -27,7 +28,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -131,6 +132,7 @@ impl ConnectorCommon for Forte { message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -167,6 +169,20 @@ impl types::PaymentsResponseData, > for Forte { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Forte".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -202,14 +218,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = forte::FortePaymentsRequest::try_from(req)?; - let forte_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(forte_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -227,7 +238,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = forte::ForteCaptureRequest::try_from(req)?; - let forte_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(forte_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -384,7 +390,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = forte::ForteCancelRequest::try_from(req)?; - let forte_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(forte_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -468,7 +469,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = forte::ForteRefundRequest::try_from(req)?; - let forte_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(forte_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -552,7 +548,7 @@ impl ConnectorIntegration, - ) -> 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..4c770eb788d4 100644 --- a/crates/router/src/connector/forte/transformers.rs +++ b/crates/router/src/connector/forte/transformers.rs @@ -54,10 +54,9 @@ impl TryFrom for ForteCardType { utils::CardIssuer::Visa => Ok(Self::Visa), utils::CardIssuer::DinersClub => Ok(Self::DinersClub), utils::CardIssuer::JCB => Ok(Self::Jcb), - _ => Err(errors::ConnectorError::NotSupported { - message: issuer.to_string(), - connector: "Forte", - } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Forte"), + ) .into()), } } @@ -67,10 +66,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { if item.request.currency != enums::Currency::USD { - Err(errors::ConnectorError::NotSupported { - message: item.request.currency.to_string(), - connector: "Forte", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Forte"), + ))? } match item.request.payment_method_data { api_models::payments::PaymentMethodData::Card(ref ccard) => { @@ -82,7 +80,10 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { let address = item.get_billing_address()?; let card = Card { card_type, - name_on_card: ccard.card_holder_name.clone(), + name_on_card: ccard + .card_holder_name + .clone() + .unwrap_or(Secret::new("".to_string())), account_number: ccard.card_number.clone(), expire_month: ccard.card_exp_month.clone(), expire_year: ccard.card_exp_year.clone(), @@ -112,11 +113,11 @@ 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(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Forte", - })? + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("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..76954217eefb 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -1,10 +1,11 @@ mod requests; +use super::utils as connector_utils; mod response; pub mod transformers; use std::fmt::Debug; -use ::common_utils::{errors::ReportSwitchExt, ext_traits::ByteSliceExt}; +use ::common_utils::{errors::ReportSwitchExt, ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; @@ -20,7 +21,6 @@ use self::{ use super::utils::RefundsRequestData; use crate::{ configs::settings, - connector::{utils as connector_utils, utils as conn_utils}, core::{ errors::{self, CustomResult}, payments, @@ -36,7 +36,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt, PaymentsCompleteAuthorize}, ErrorResponse, }, - utils::{self, crypto, BytesExt}, + utils::{crypto, BytesExt}, }; #[derive(Debug, Clone)] @@ -105,6 +105,7 @@ impl ConnectorCommon for Globalpay { message: response.detailed_error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -166,10 +167,8 @@ impl &self, _req: &types::PaymentsCompleteAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let globalpay_req = types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + ) -> CustomResult { + Ok(RequestContent::Json(Box::new(serde_json::json!({})))) } fn build_request( @@ -187,7 +186,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -264,7 +263,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = GlobalpayRefreshTokenRequest::try_from(req)?; - let globalpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + ) -> CustomResult { + let connector_req = GlobalpayRefreshTokenRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -319,6 +313,7 @@ impl ConnectorIntegration for Globalpay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Globalpay".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Globalpay {} @@ -387,7 +396,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = requests::GlobalpayCancelRequest::try_from(req)?; - let globalpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + ) -> CustomResult { + let connector_req = requests::GlobalpayCancelRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -549,14 +553,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = requests::GlobalpayCaptureRequest::try_from(req)?; - let globalpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + ) -> CustomResult { + let connector_req = requests::GlobalpayCaptureRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -572,7 +571,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = GlobalpayPaymentsRequest::try_from(req)?; - let globalpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + ) -> CustomResult { + let connector_req = GlobalpayPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -666,7 +660,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = requests::GlobalpayRefundRequest::try_from(req)?; - let globalpay_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -756,7 +745,7 @@ impl ConnectorIntegration, _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let signature = conn_utils::get_header_key_value("x-gp-signature", request.headers)?; + let signature = connector_utils::get_header_key_value("x-gp-signature", request.headers)?; Ok(signature.as_bytes().to_vec()) } @@ -932,14 +918,15 @@ impl api::IncomingWebhook for Globalpay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> 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..3124501bd6e5 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, }), } } @@ -384,7 +385,7 @@ fn get_payment_method_data( api::PaymentMethodData::Card(ccard) => Ok(PaymentMethodData::Card(requests::Card { number: ccard.card_number.clone(), expiry_month: ccard.card_exp_month.clone(), - expiry_year: ccard.get_card_expiry_year_2_digit(), + expiry_year: ccard.get_card_expiry_year_2_digit()?, cvv: ccard.card_cvc.clone(), account_type: None, authcode: None, diff --git a/crates/router/src/connector/globepay.rs b/crates/router/src/connector/globepay.rs index 547bf66fb7d5..8f8b01de8efe 100644 --- a/crates/router/src/connector/globepay.rs +++ b/crates/router/src/connector/globepay.rs @@ -2,7 +2,10 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::crypto::{self, GenerateDigest}; +use common_utils::{ + crypto::{self, GenerateDigest}, + request::RequestContent, +}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use hex::encode; @@ -22,7 +25,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -123,6 +126,7 @@ impl ConnectorCommon for Globepay { message: consts::NO_ERROR_MESSAGE.to_string(), reason: Some(response.return_msg), attempt_status: None, + connector_transaction_id: None, }) } } @@ -146,6 +150,20 @@ impl types::PaymentsResponseData, > for Globepay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Globepay".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -189,14 +207,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = globepay::GlobepayPaymentsRequest::try_from(req)?; - let globepay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globepay_req)) + ) -> CustomResult { + let connector_req = globepay::GlobepayPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -214,7 +227,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = globepay::GlobepayRefundRequest::try_from(req)?; - let globepay_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globepay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -391,7 +399,7 @@ impl ConnectorIntegration, - ) -> 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..a874fc21d297 100644 --- a/crates/router/src/connector/globepay/transformers.rs +++ b/crates/router/src/connector/globepay/transformers.rs @@ -3,7 +3,7 @@ use masking::Secret; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::RouterData, + connector::utils::{self, RouterData}, consts, core::errors, types::{self, api, storage::enums}, @@ -31,12 +31,47 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for GlobepayPaymentsRequest { api::PaymentMethodData::Wallet(ref wallet_data) => match wallet_data { api::WalletData::AliPayQr(_) => GlobepayChannel::Alipay, api::WalletData::WeChatPayQr(_) => GlobepayChannel::Wechat, - _ => Err(errors::ConnectorError::NotImplemented( - "Payment method".to_string(), + api::WalletData::AliPayRedirect(_) + | api::WalletData::AliPayHkRedirect(_) + | api::WalletData::MomoRedirect(_) + | api::WalletData::KakaoPayRedirect(_) + | api::WalletData::GoPayRedirect(_) + | api::WalletData::GcashRedirect(_) + | api::WalletData::ApplePay(_) + | api::WalletData::ApplePayRedirect(_) + | api::WalletData::ApplePayThirdPartySdk(_) + | api::WalletData::DanaRedirect {} + | api::WalletData::GooglePay(_) + | api::WalletData::GooglePayRedirect(_) + | api::WalletData::GooglePayThirdPartySdk(_) + | api::WalletData::MbWayRedirect(_) + | api::WalletData::MobilePayRedirect(_) + | api::WalletData::PaypalRedirect(_) + | api::WalletData::PaypalSdk(_) + | api::WalletData::SamsungPay(_) + | api::WalletData::TwintRedirect {} + | api::WalletData::VippsRedirect {} + | api::WalletData::TouchNGoRedirect(_) + | api::WalletData::WeChatPayRedirect(_) + | api::WalletData::CashappQr(_) + | api::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("globepay"), ))?, }, - _ => Err(errors::ConnectorError::NotImplemented( - "Payment method".to_string(), + api::PaymentMethodData::Card(_) + | api::PaymentMethodData::CardRedirect(_) + | api::PaymentMethodData::PayLater(_) + | api::PaymentMethodData::BankRedirect(_) + | api::PaymentMethodData::BankDebit(_) + | api::PaymentMethodData::BankTransfer(_) + | api::PaymentMethodData::Crypto(_) + | api::PaymentMethodData::MandatePayment + | api::PaymentMethodData::Reward + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("globepay"), ))?, }; let description = item.get_description()?; @@ -157,6 +192,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -230,6 +266,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -258,6 +295,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..542caa73ff33 100644 --- a/crates/router/src/connector/gocardless.rs +++ b/crates/router/src/connector/gocardless.rs @@ -3,7 +3,7 @@ pub mod transformers; use std::fmt::Debug; use api_models::enums::enums; -use common_utils::{crypto, ext_traits::ByteSliceExt}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as gocardless; @@ -23,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -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, }) } } @@ -154,14 +155,9 @@ impl &self, req: &types::ConnectorCustomerRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = gocardless::GocardlessCustomerRequest::try_from(req)?; - let gocardless_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(gocardless_req)) + ) -> CustomResult { + let connector_req = gocardless::GocardlessCustomerRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -179,7 +175,7 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body( + .set_body(types::ConnectorCustomerType::get_request_body( self, req, connectors, )?) .build(), @@ -252,14 +248,9 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = gocardless::GocardlessBankAccountRequest::try_from(req)?; - let gocardless_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(gocardless_req)) + ) -> CustomResult { + let connector_req = gocardless::GocardlessBankAccountRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -273,7 +264,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -373,14 +364,9 @@ impl &self, req: &types::SetupMandateRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = gocardless::GocardlessMandateRequest::try_from(req)?; - let gocardless_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(gocardless_req)) + ) -> CustomResult { + let connector_req = gocardless::GocardlessMandateRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -396,7 +382,7 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body( + .set_body(types::SetupMandateType::get_request_body( self, req, connectors, )?) .build(), @@ -457,20 +443,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = gocardless::GocardlessRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = gocardless::GocardlessPaymentsRequest::try_from(&connector_router_data)?; - let gocardless_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(gocardless_req)) + let connector_req = + gocardless::GocardlessPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -488,7 +470,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = gocardless::GocardlessRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = gocardless::GocardlessRefundRequest::try_from(&connector_router_data)?; - let gocardless_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(gocardless_req)) + let connector_req = gocardless::GocardlessRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -653,7 +630,7 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: gocardless::GocardlessWebhookEvent = request .body .parse_struct("GocardlessWebhookEvent") @@ -851,19 +828,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..0b2cac5d7660 100644 --- a/crates/router/src/connector/helcim.rs +++ b/crates/router/src/connector/helcim.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::ExposeInterface; @@ -127,9 +128,12 @@ impl ConnectorCommon for Helcim { .response .parse_struct("HelcimErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - let error_string = match response.errors { - transformers::HelcimErrorTypes::StringType(error) => error, - transformers::HelcimErrorTypes::JsonType(error) => error.to_string(), + let error_string = match response { + transformers::HelcimErrorResponse::Payment(response) => match response.errors { + transformers::HelcimErrorTypes::StringType(error) => error, + transformers::HelcimErrorTypes::JsonType(error) => error.to_string(), + }, + transformers::HelcimErrorResponse::General(error_string) => error_string, }; Ok(ErrorResponse { @@ -138,6 +142,7 @@ impl ConnectorCommon for Helcim { message: error_string.clone(), reason: Some(error_string), attempt_status: None, + connector_transaction_id: None, }) } } @@ -194,32 +199,32 @@ impl &self, req: &types::SetupMandateRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = helcim::HelcimVerifyRequest::try_from(req)?; - let helcim_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(helcim_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, - req: &types::SetupMandateRouterData, - connectors: &settings::Connectors, + _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(), - )) + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Helcim".to_string()) + .into(), + ) + + // 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)?) + // .set_body(types::SetupMandateType::get_request_body( + // self, req, connectors, + // )?) + // .build(), + // )) } fn handle_response( &self, @@ -274,20 +279,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = helcim::HelcimPaymentsRequest::try_from(&connector_router_data)?; - let helcim_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(helcim_req)) + let connector_req = helcim::HelcimPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -305,7 +305,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -456,12 +456,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(helcim_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -477,7 +472,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = helcim::HelcimVoidRequest::try_from(req)?; - let helcim_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(helcim_req)) + ) -> CustomResult { + let connector_req = helcim::HelcimVoidRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -556,7 +546,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = helcim::HelcimRefundRequest::try_from(&connector_router_data)?; - let helcim_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(helcim_req)) + let connector_req = helcim::HelcimRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -641,7 +626,7 @@ impl ConnectorIntegration, - ) -> 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..5e076be651f5 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -44,6 +44,19 @@ impl } } +pub fn check_currency( + currency: types::storage::enums::Currency, +) -> Result { + if currency == types::storage::enums::Currency::USD { + Ok(currency) + } else { + Err(errors::ConnectorError::NotSupported { + message: format!("currency {currency} is not supported for this merchant account"), + connector: "Helcim", + })? + } +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct HelcimVerifyRequest { @@ -62,6 +75,7 @@ pub struct HelcimPaymentsRequest { currency: enums::Currency, ip_address: Secret, card_data: HelcimCard, + invoice: HelcimInvoice, billing_address: HelcimBillingAddress, //The ecommerce field is an optional field in Connector Helcim. //Setting the ecommerce field to true activates the Helcim Fraud Defender. @@ -83,6 +97,22 @@ pub struct HelcimBillingAddress { email: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HelcimInvoice { + invoice_number: String, + line_items: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HelcimLineItems { + description: String, + quantity: u8, + price: f64, + total: f64, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct HelcimCard { @@ -96,7 +126,8 @@ impl TryFrom<(&types::SetupMandateRouterData, &api::Card)> for HelcimVerifyReque fn try_from(value: (&types::SetupMandateRouterData, &api::Card)) -> Result { let (item, req_card) = value; let card_data = HelcimCard { - card_expiry: req_card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + card_expiry: req_card + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, card_number: req_card.card_number.clone(), card_c_v_v: req_card.card_cvc.clone(), }; @@ -111,9 +142,9 @@ impl TryFrom<(&types::SetupMandateRouterData, &api::Card)> for HelcimVerifyReque email: item.request.email.clone(), }; let ip_address = item.request.get_browser_info()?.get_ip_address()?; - + let currency = check_currency(item.request.currency)?; Ok(Self { - currency: item.request.currency, + currency, ip_address, card_data, billing_address, @@ -141,11 +172,11 @@ 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(_) => { - Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.request.payment_method_data), - connector: "Helcim", - })? + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Helcim"), + ))? } } } @@ -166,7 +197,8 @@ impl ) -> Result { let (item, req_card) = value; let card_data = HelcimCard { - card_expiry: req_card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + card_expiry: req_card + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, card_number: req_card.card_number.clone(), card_c_v_v: req_card.card_cvc.clone(), }; @@ -191,11 +223,30 @@ impl .request .get_browser_info()? .get_ip_address()?; + let line_items = vec![ + (HelcimLineItems { + description: item + .router_data + .description + .clone() + .unwrap_or("No Description".to_string()), + // By default quantity is set to 1 and price and total is set to amount because these three fields are required to generate an invoice. + quantity: 1, + price: item.amount, + total: item.amount, + }), + ]; + let invoice = HelcimInvoice { + invoice_number: item.router_data.connector_request_reference_id.clone(), + line_items, + }; + let currency = check_currency(item.router_data.request.currency)?; Ok(Self { - amount: item.amount.to_owned(), - currency: item.router_data.request.currency, + amount: item.amount, + currency, ip_address, card_data, + invoice, billing_address, ecommerce: None, }) @@ -223,12 +274,10 @@ 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::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Helcim"), + ))?, } } } @@ -295,6 +344,7 @@ impl From for enums::AttemptStatus { pub struct HelcimPaymentsResponse { status: HelcimPaymentStatus, transaction_id: u64, + invoice_number: Option, #[serde(rename = "type")] transaction_type: HelcimTransactionType, } @@ -327,7 +377,8 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item.response.invoice_number.clone(), + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -381,7 +432,8 @@ impl mandate_reference: None, connector_metadata, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item.response.invoice_number.clone(), + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -439,7 +491,8 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item.response.invoice_number.clone(), + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -525,7 +578,8 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item.response.invoice_number.clone(), + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -587,7 +641,8 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item.response.invoice_number.clone(), + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -701,6 +756,13 @@ pub enum HelcimErrorTypes { } #[derive(Debug, Deserialize)] -pub struct HelcimErrorResponse { +pub struct HelcimPaymentsErrorResponse { pub errors: HelcimErrorTypes, } + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum HelcimErrorResponse { + Payment(HelcimPaymentsErrorResponse), + General(String), +} diff --git a/crates/router/src/connector/iatapay.rs b/crates/router/src/connector/iatapay.rs index 008047c1d366..c5ac0f43833c 100644 --- a/crates/router/src/connector/iatapay.rs +++ b/crates/router/src/connector/iatapay.rs @@ -3,7 +3,7 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; -use common_utils::{crypto, ext_traits::ByteSliceExt}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as iatapay; @@ -25,7 +25,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{BytesExt, Encode}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -125,6 +125,7 @@ impl ConnectorCommon for Iatapay { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -179,14 +180,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = iatapay::IatapayAuthUpdateRequest::try_from(req)?; - let iatapay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(iatapay_req)) + ) -> CustomResult { + let connector_req = iatapay::IatapayAuthUpdateRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -200,7 +196,7 @@ impl ConnectorIntegration for Iatapay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Iatapay".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -280,20 +291,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = iatapay::IatapayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = iatapay::IatapayPaymentsRequest::try_from(&connector_router_data)?; - let iatapay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(iatapay_req)) + let connector_req = iatapay::IatapayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -311,7 +317,7 @@ impl ConnectorIntegration CustomResult { - let connector_id = req - .request - .connector_transaction_id - .get_connector_transaction_id() - .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + let auth: iatapay::IatapayAuthType = + iatapay::IatapayAuthType::try_from(&req.connector_auth_type)?; + let merchant_id = auth.merchant_id.peek(); Ok(format!( - "{}/payments/{}", + "{}/merchants/{merchant_id}/payments/{}", self.base_url(connectors), - connector_id + req.connector_request_reference_id.clone() )) } @@ -469,20 +473,15 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = iatapay::IatapayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, - req.request.payment_amount, + req.request.refund_amount, req, ))?; - let req_obj = iatapay::IatapayRefundRequest::try_from(&connector_router_data)?; - let iatapay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(iatapay_req)) + let connector_req = iatapay::IatapayRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -497,7 +496,7 @@ impl ConnectorIntegration, ) -> CustomResult { - let notif: IatapayPaymentsResponse = - request - .body - .parse_struct("IatapayPaymentsResponse") - .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; - if notif.iata_payment_id.is_some() { - Ok(api_models::webhooks::ObjectReferenceId::PaymentId( - api_models::payments::PaymentIdType::ConnectorTransactionId( - notif.iata_payment_id.unwrap_or_default(), - ), - )) - } else { - Ok(api_models::webhooks::ObjectReferenceId::RefundId( - api_models::webhooks::RefundIdType::ConnectorRefundId( - notif.iata_refund_id.unwrap_or_default(), - ), - )) + let notif: iatapay::IatapayWebhookResponse = request + .body + .parse_struct("IatapayWebhookResponse") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + match notif { + iatapay::IatapayWebhookResponse::IatapayPaymentWebhookBody(wh_body) => { + match wh_body.merchant_payment_id { + Some(merchant_payment_id) => { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + merchant_payment_id, + ), + )) + } + None => Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + wh_body.iata_payment_id, + ), + )), + } + } + iatapay::IatapayWebhookResponse::IatapayRefundWebhookBody(wh_body) => { + match wh_body.merchant_refund_id { + Some(merchant_refund_id) => { + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::RefundId(merchant_refund_id), + )) + } + None => Ok(api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId( + wh_body.iata_refund_id, + ), + )), + } + } } } @@ -660,44 +674,29 @@ impl api::IncomingWebhook for Iatapay { &self, request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - let notif: IatapayPaymentsResponse = - request - .body - .parse_struct("IatapayPaymentsResponse") - .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - match notif.status { - iatapay::IatapayPaymentStatus::Authorized => match notif.iata_payment_id.is_some() { - true => Ok(api::IncomingWebhookEvent::PaymentIntentSuccess), - false => Ok(api::IncomingWebhookEvent::RefundSuccess), - }, - iatapay::IatapayPaymentStatus::Failed => match notif.iata_payment_id.is_some() { - true => Ok(api::IncomingWebhookEvent::PaymentIntentFailure), - false => Ok(api::IncomingWebhookEvent::RefundFailure), - }, - iatapay::IatapayPaymentStatus::Unknown - | iatapay::IatapayPaymentStatus::Created - | iatapay::IatapayPaymentStatus::Initiated - | iatapay::IatapayPaymentStatus::Cleared - | iatapay::IatapayPaymentStatus::Settled - | iatapay::IatapayPaymentStatus::Tobeinvestigated - | iatapay::IatapayPaymentStatus::Blocked - | iatapay::IatapayPaymentStatus::Locked - | iatapay::IatapayPaymentStatus::UnexpectedSettled => { - Ok(api::IncomingWebhookEvent::EventNotSupported) - } - } + let notif: iatapay::IatapayWebhookResponse = request + .body + .parse_struct("IatapayWebhookResponse") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + api::IncomingWebhookEvent::try_from(notif) } fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { - let notif: IatapayPaymentsResponse = - request - .body - .parse_struct("IatapayPaymentsResponse") - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + ) -> CustomResult, errors::ConnectorError> { + let notif: iatapay::IatapayWebhookResponse = request + .body + .parse_struct("IatapayWebhookResponse") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + match notif { + iatapay::IatapayWebhookResponse::IatapayPaymentWebhookBody(wh_body) => { + Ok(Box::new(wh_body)) + } + iatapay::IatapayWebhookResponse::IatapayRefundWebhookBody(refund_wh_body) => { + Ok(Box::new(refund_wh_body)) + } + } } } diff --git a/crates/router/src/connector/iatapay/transformers.rs b/crates/router/src/connector/iatapay/transformers.rs index 7cdfafc858b6..4557fda03904 100644 --- a/crates/router/src/connector/iatapay/transformers.rs +++ b/crates/router/src/connector/iatapay/transformers.rs @@ -1,16 +1,22 @@ use std::collections::HashMap; use api_models::enums::PaymentMethod; +use common_utils::errors::CustomResult; use masking::{Secret, SwitchStrategy}; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData}, + connector::utils::{ + self as connector_util, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData, + }, + consts, core::errors, services, types::{self, api, storage::enums, PaymentsAuthorizeData}, }; +type Error = error_stack::Report; + // Every access token will be valid for 5 minutes. It contains grant_type and scope for different type of access, but for our usecases it should be only 'client_credentials' and 'payment' resp(as per doc) for all type of api call. #[derive(Debug, Serialize)] pub struct IatapayAuthUpdateRequest { @@ -41,7 +47,7 @@ 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, @@ -49,7 +55,7 @@ impl ), ) -> Result { Ok(Self { - amount: utils::to_currency_base_unit_asf64(_amount, _currency)?, + amount: connector_util::get_amount_as_f64(currency_unit, amount, currency)?, router_data: item, }) } @@ -132,7 +138,6 @@ impl let payment_method = item.router_data.payment_method; let country = match payment_method { PaymentMethod::Upi => "IN".to_string(), - PaymentMethod::Card | PaymentMethod::CardRedirect | PaymentMethod::PayLater @@ -150,7 +155,19 @@ impl api::PaymentMethodData::Upi(upi_data) => upi_data.vpa_id.map(|id| PayerInfo { token_id: id.switch_strategy(), }), - _ => None, + api::PaymentMethodData::Card(_) + | api::PaymentMethodData::CardRedirect(_) + | api::PaymentMethodData::Wallet(_) + | api::PaymentMethodData::PayLater(_) + | api::PaymentMethodData::BankRedirect(_) + | api::PaymentMethodData::BankDebit(_) + | api::PaymentMethodData::BankTransfer(_) + | api::PaymentMethodData::Crypto(_) + | api::PaymentMethodData::MandatePayment + | api::PaymentMethodData::Reward + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => None, }; let payload = Self { merchant_id: IatapayAuthType::try_from(&item.router_data.connector_auth_type)? @@ -208,25 +225,21 @@ pub enum IatapayPaymentStatus { Initiated, Authorized, Settled, - Tobeinvestigated, - Blocked, Cleared, Failed, - Locked, #[serde(rename = "UNEXPECTED SETTLED")] UnexpectedSettled, - #[serde(other)] - Unknown, } impl From for enums::AttemptStatus { fn from(item: IatapayPaymentStatus) -> Self { match item { - IatapayPaymentStatus::Authorized | IatapayPaymentStatus::Settled => Self::Charged, + IatapayPaymentStatus::Authorized + | IatapayPaymentStatus::Settled + | IatapayPaymentStatus::Cleared => Self::Charged, IatapayPaymentStatus::Failed | IatapayPaymentStatus::UnexpectedSettled => Self::Failure, IatapayPaymentStatus::Created => Self::AuthenticationPending, IatapayPaymentStatus::Initiated => Self::Pending, - _ => Self::Voided, } } } @@ -252,56 +265,87 @@ pub struct IatapayPaymentsResponse { pub merchant_payment_id: Option, pub amount: f64, pub currency: String, - pub country: Option, - pub locale: Option, - pub bank_transfer_description: Option, pub checkout_methods: Option, pub failure_code: Option, + pub failure_details: Option, +} + +fn get_iatpay_response( + response: IatapayPaymentsResponse, + status_code: u16, +) -> CustomResult< + ( + enums::AttemptStatus, + Option, + types::PaymentsResponseData, + ), + errors::ConnectorError, +> { + let status = enums::AttemptStatus::from(response.status); + let error = if connector_util::is_payment_failure(status) { + Some(types::ErrorResponse { + code: response + .failure_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .failure_details + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.failure_details, + status_code, + attempt_status: Some(status), + connector_transaction_id: response.iata_payment_id.clone(), + }) + } else { + None + }; + let form_fields = HashMap::new(); + let id = match response.iata_payment_id.clone() { + Some(s) => types::ResponseId::ConnectorTransactionId(s), + None => types::ResponseId::NoResponseId, + }; + let connector_response_reference_id = response.merchant_payment_id.or(response.iata_payment_id); + + let payment_response_data = response.checkout_methods.map_or( + types::PaymentsResponseData::TransactionResponse { + resource_id: id.clone(), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: connector_response_reference_id.clone(), + incremental_authorization_allowed: None, + }, + |checkout_methods| types::PaymentsResponseData::TransactionResponse { + resource_id: id, + redirection_data: Some(services::RedirectForm::Form { + endpoint: checkout_methods.redirect.redirect_url, + method: services::Method::Get, + form_fields, + }), + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: connector_response_reference_id.clone(), + incremental_authorization_allowed: None, + }, + ); + Ok((status, error, payment_response_data)) } impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::ResponseRouterData, ) -> Result { - let form_fields = HashMap::new(); - let id = match item.response.iata_payment_id.clone() { - Some(s) => types::ResponseId::ConnectorTransactionId(s), - None => types::ResponseId::NoResponseId, - }; - let connector_response_reference_id = item - .response - .merchant_payment_id - .or(item.response.iata_payment_id); + let (status, error, payment_response_data) = + get_iatpay_response(item.response, item.http_code)?; Ok(Self { - status: enums::AttemptStatus::from(item.response.status), - response: item.response.checkout_methods.map_or( - Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: id.clone(), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: connector_response_reference_id.clone(), - }), - |checkout_methods| { - Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: id, - redirection_data: Some(services::RedirectForm::Form { - endpoint: checkout_methods.redirect.redirect_url, - method: services::Method::Get, - form_fields, - }), - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: connector_response_reference_id.clone(), - }) - }, - ), + status, + response: error.map_or_else(|| Ok(payment_response_data), Err), ..item.data }) } @@ -395,11 +439,32 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { + let refund_status = enums::RefundStatus::from(item.response.status); + let response = if connector_util::is_refund_failure(refund_status) { + Err(types::ErrorResponse { + code: item + .response + .failure_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: item + .response + .failure_details + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: item.response.failure_details, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(item.response.iata_refund_id.clone()), + }) + } else { + Ok(types::RefundsResponseData { connector_refund_id: item.response.iata_refund_id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), + refund_status, + }) + }; + + Ok(Self { + response, ..item.data }) } @@ -412,11 +477,31 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { + let refund_status = enums::RefundStatus::from(item.response.status); + let response = if connector_util::is_refund_failure(refund_status) { + Err(types::ErrorResponse { + code: item + .response + .failure_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: item + .response + .failure_details + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: item.response.failure_details, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(item.response.iata_refund_id.clone()), + }) + } else { + Ok(types::RefundsResponseData { connector_refund_id: item.response.iata_refund_id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), + refund_status, + }) + }; + Ok(Self { + response, ..item.data }) } @@ -435,3 +520,96 @@ pub struct IatapayAccessTokenErrorResponse { pub error: String, pub path: String, } + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IatapayPaymentWebhookBody { + pub status: IatapayWebhookStatus, + pub iata_payment_id: String, + pub merchant_payment_id: Option, + pub failure_code: Option, + pub failure_details: Option, + pub amount: f64, + pub currency: String, + pub checkout_methods: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IatapayRefundWebhookBody { + pub status: IatapayRefundWebhookStatus, + pub iata_refund_id: String, + pub merchant_refund_id: Option, + pub failure_code: Option, + pub failure_details: Option, + pub amount: f64, + pub currency: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum IatapayWebhookResponse { + IatapayPaymentWebhookBody(IatapayPaymentWebhookBody), + IatapayRefundWebhookBody(IatapayRefundWebhookBody), +} + +impl TryFrom for api::IncomingWebhookEvent { + type Error = error_stack::Report; + fn try_from(payload: IatapayWebhookResponse) -> CustomResult { + match payload { + IatapayWebhookResponse::IatapayPaymentWebhookBody(wh_body) => match wh_body.status { + IatapayWebhookStatus::Authorized | IatapayWebhookStatus::Settled => { + Ok(Self::PaymentIntentSuccess) + } + IatapayWebhookStatus::Initiated => Ok(Self::PaymentIntentProcessing), + IatapayWebhookStatus::Failed => Ok(Self::PaymentIntentFailure), + IatapayWebhookStatus::Created + | IatapayWebhookStatus::Cleared + | IatapayWebhookStatus::Tobeinvestigated + | IatapayWebhookStatus::Blocked + | IatapayWebhookStatus::UnexpectedSettled + | IatapayWebhookStatus::Unknown => Ok(Self::EventNotSupported), + }, + IatapayWebhookResponse::IatapayRefundWebhookBody(wh_body) => match wh_body.status { + IatapayRefundWebhookStatus::Authorized | IatapayRefundWebhookStatus::Settled => { + Ok(Self::RefundSuccess) + } + IatapayRefundWebhookStatus::Failed => Ok(Self::RefundFailure), + IatapayRefundWebhookStatus::Created + | IatapayRefundWebhookStatus::Locked + | IatapayRefundWebhookStatus::Initiated + | IatapayRefundWebhookStatus::Unknown => Ok(Self::EventNotSupported), + }, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum IatapayWebhookStatus { + Created, + Initiated, + Authorized, + Settled, + Cleared, + Failed, + Tobeinvestigated, + Blocked, + #[serde(rename = "UNEXPECTED SETTLED")] + UnexpectedSettled, + #[serde(other)] + Unknown, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum IatapayRefundWebhookStatus { + Created, + Initiated, + Authorized, + Settled, + Failed, + Locked, + #[serde(other)] + Unknown, +} diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 3670f65a2f02..5361a636826d 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; use api_models::payments as api_payments; +use common_utils::request::RequestContent; use error_stack::{IntoReport, ResultExt}; use transformers as klarna; @@ -21,7 +22,7 @@ use crate::{ api::{self, ConnectorCommon}, storage::enums as storage_enums, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -76,6 +77,7 @@ impl ConnectorCommon for Klarna { message: consts::NO_ERROR_MESSAGE.to_string(), reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -155,15 +157,10 @@ impl &self, req: &types::PaymentsSessionRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = klarna::KlarnaSessionRequest::try_from(req)?; // encode only for for urlencoded things. - let klarna_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(klarna_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -179,7 +176,7 @@ impl .headers(types::PaymentsSessionType::get_headers( self, req, connectors, )?) - .body(types::PaymentsSessionType::get_request_body( + .set_body(types::PaymentsSessionType::get_request_body( self, req, connectors, )?) .build(), @@ -221,6 +218,20 @@ impl > for Klarna { // Not Implemented(R) + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Klarna".to_string()) + .into(), + ) + } } impl @@ -405,7 +416,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 )), } @@ -415,7 +427,7 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = klarna::KlarnaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -423,12 +435,7 @@ impl req, ))?; let connector_req = klarna::KlarnaPaymentsRequest::try_from(&connector_router_data)?; - let klarna_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(klarna_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -446,7 +453,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -520,7 +527,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..cee4225d861e 100644 --- a/crates/router/src/connector/mollie.rs +++ b/crates/router/src/connector/mollie.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as mollie; @@ -24,7 +25,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -99,6 +100,7 @@ impl ConnectorCommon for Mollie { message: response.detail, reason: response.field, attempt_status: None, + connector_transaction_id: None, }) } } @@ -151,14 +153,9 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = mollie::MollieCardTokenRequest::try_from(req)?; - let mollie_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(mollie_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -171,7 +168,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -212,6 +209,20 @@ impl types::PaymentsResponseData, > for Mollie { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Mollie".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -237,20 +248,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = mollie::MollieRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = mollie::MolliePaymentsRequest::try_from(&router_obj)?; - let mollie_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(mollie_req)) + let connector_req = mollie::MolliePaymentsRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -268,7 +274,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = mollie::MollieRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = mollie::MollieRefundRequest::try_from(&router_obj)?; - let mollie_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(mollie_req)) + let connector_req = mollie::MollieRefundRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -462,7 +463,7 @@ impl ConnectorIntegration, - ) -> 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..0fcb14d2cf59 100644 --- a/crates/router/src/connector/mollie/transformers.rs +++ b/crates/router/src/connector/mollie/transformers.rs @@ -286,10 +286,13 @@ impl TryFrom<&types::TokenizationRouterData> for MollieCardTokenRequest { match item.request.payment_method_data.clone() { api_models::payments::PaymentMethodData::Card(ccard) => { let auth = MollieAuthType::try_from(&item.connector_auth_type)?; - let card_holder = ccard.card_holder_name.clone(); + let card_holder = ccard + .card_holder_name + .clone() + .unwrap_or(Secret::new("".to_string())); let card_number = ccard.card_number.clone(); let card_expiry_date = - ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()); + ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned())?; let card_cvv = ccard.card_cvc; let locale = item.request.get_browser_info()?.get_language()?; let testmode = @@ -531,6 +534,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..8290a00661c2 100644 --- a/crates/router/src/connector/multisafepay.rs +++ b/crates/router/src/connector/multisafepay.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use error_stack::{IntoReport, ResultExt}; use masking::ExposeInterface; use transformers as multisafepay; @@ -21,7 +22,7 @@ use crate::{ storage::enums, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -84,6 +85,7 @@ impl ConnectorCommon for Multisafepay { message: response.error_info, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -128,6 +130,20 @@ impl types::PaymentsResponseData, > for Multisafepay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for Multisafepay".to_string(), + ) + .into()) + } } impl api::PaymentVoid for Multisafepay {} @@ -263,20 +279,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = multisafepay::MultisafepayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = multisafepay::MultisafepayPaymentsRequest::try_from(&connector_router_data)?; - let multisafepay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(multisafepay_req)) + let connector_req = + multisafepay::MultisafepayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -294,7 +306,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = multisafepay::MultisafepayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = multisafepay::MultisafepayRefundRequest::try_from(&connector_req)?; + let connector_req = multisafepay::MultisafepayRefundRequest::try_from(&connector_req)?; - let multisafepay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(multisafepay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -396,7 +403,7 @@ impl ConnectorIntegration, - ) -> 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 ee70d26ffbda..86096ed508b6 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -262,10 +262,9 @@ impl TryFrom for Gateway { utils::CardIssuer::Visa => Ok(Self::Visa), utils::CardIssuer::DinersClub | utils::CardIssuer::JCB - | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotSupported { - message: issuer.to_string(), - connector: "Multisafe pay", - } + | 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"), ))?, }; @@ -426,7 +426,7 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> card_expiry_date: Some( (format!( "{}{}", - ccard.get_card_expiry_year_2_digit().expose(), + ccard.get_card_expiry_year_2_digit()?.expose(), ccard.card_exp_month.clone().expose() )) .parse::() @@ -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 for Nexinets { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Nexinets".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -201,14 +217,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nexinets::NexinetsPaymentsRequest::try_from(req)?; - let nexinets_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nexinets_req)) + ) -> CustomResult { + let connector_req = nexinets::NexinetsPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -226,7 +237,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?; - let nexinets_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nexinets_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -389,7 +395,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?; - let nexinets_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nexinets_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -473,7 +474,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = nexinets::NexinetsRefundRequest::try_from(req)?; - let nexinets_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nexinets_req)) + ) -> CustomResult { + let connector_req = nexinets::NexinetsRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -561,7 +557,7 @@ impl ConnectorIntegration, - ) -> 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..698b2709a62d 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"), ))?, } @@ -640,7 +643,7 @@ fn get_card_data( Some(true) => CardDataDetails::PaymentInstrument(Box::new(PaymentInstrument { payment_instrument_id: item.request.connector_mandate_id(), })), - _ => CardDataDetails::CardDetails(Box::new(get_card_details(card))), + _ => CardDataDetails::CardDetails(Box::new(get_card_details(card)?)), }; let cof_contract = Some(CofContract { recurring_type: RecurringType::Unscheduled, @@ -648,7 +651,7 @@ fn get_card_data( (card_data, cof_contract) } false => ( - CardDataDetails::CardDetails(Box::new(get_card_details(card))), + CardDataDetails::CardDetails(Box::new(get_card_details(card)?)), None, ), }; @@ -674,13 +677,15 @@ fn get_applepay_details( }) } -fn get_card_details(req_card: &api_models::payments::Card) -> CardDetails { - CardDetails { +fn get_card_details( + req_card: &api_models::payments::Card, +) -> Result { + Ok(CardDetails { card_number: req_card.card_number.clone(), expiry_month: req_card.card_exp_month.clone(), - expiry_year: req_card.get_card_expiry_year_2_digit(), + expiry_year: req_card.get_card_expiry_year_2_digit()?, verification: req_card.card_cvc.clone(), - } + }) } fn get_wallet_details( diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index d7e9cd78bb88..0550908649ff 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -2,23 +2,23 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::ext_traits::ByteSliceExt; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; +use regex::Regex; use transformers as nmi; -use self::transformers::NmiCaptureRequest; +use super::utils as connector_utils; use crate::{ configs::settings, - connector::utils as connector_utils, core::errors::{self, CustomResult}, services::{self, request, ConnectorIntegration, ConnectorValidation}, types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, + transformers::ForeignFrom, ErrorResponse, }, - utils, }; #[derive(Clone, Debug)] @@ -75,10 +75,12 @@ impl ConnectorCommon for Nmi { .parse_struct("StandardResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; Ok(ErrorResponse { - message: response.responsetext, + message: response.responsetext.to_owned(), status_code: res.status_code, - reason: None, - ..Default::default() + reason: Some(response.responsetext), + code: response.response_code, + attempt_status: None, + connector_transaction_id: Some(response.transactionid), }) } } @@ -96,6 +98,14 @@ impl ConnectorValidation for Nmi { ), } } + + fn validate_psync_reference_id( + &self, + _data: &types::PaymentsSyncRouterData, + ) -> CustomResult<(), errors::ConnectorError> { + // in case we dont have transaction id, we can make psync using attempt id + Ok(()) + } } impl @@ -144,39 +154,120 @@ impl &self, req: &types::SetupMandateRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nmi::NmiPaymentsRequest::try_from(req)?; - let nmi_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( &self, - req: &types::SetupMandateRouterData, + _req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("Setup Mandate flow for Nmi".to_string()).into()) + + // Ok(Some( + // services::RequestBuilder::new() + // .method(services::Method::Post) + // .url(&types::SetupMandateType::get_url(self, req, connectors)?) + // .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + // .set_body(types::SetupMandateType::get_request_body( + // self, req, connectors, + // )?) + // .build(), + // )) + } + + fn handle_response( + &self, + data: &types::SetupMandateRouterData, + res: types::Response, + ) -> CustomResult { + let response: nmi::StandardResponse = serde_urlencoded::from_bytes(&res.response) + .into_report() + .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 api::PaymentsPreProcessing for Nmi {} + +impl + ConnectorIntegration< + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > for Nmi +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + 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::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}api/transact.php", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::PaymentsPreProcessingRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = nmi::NmiVaultRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(Some( + let req = Some( services::RequestBuilder::new() .method(services::Method::Post) - .url(&types::SetupMandateType::get_url(self, req, connectors)?) - .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body( + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .set_body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), - )) + ); + Ok(req) } fn handle_response( &self, - data: &types::SetupMandateRouterData, + data: &types::PaymentsPreProcessingRouterData, res: types::Response, - ) -> CustomResult { - let response: nmi::StandardResponse = serde_urlencoded::from_bytes(&res.response) + ) -> CustomResult { + let response: nmi::NmiVaultResponse = serde_urlencoded::from_bytes(&res.response) .into_report() .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { @@ -217,7 +308,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = nmi::NmiRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -225,12 +316,7 @@ impl ConnectorIntegration::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -247,7 +333,7 @@ impl ConnectorIntegration for Nmi +{ + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + 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::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}api/transact.php", self.base_url(connectors))) + } + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = nmi::NmiRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = nmi::NmiCompleteRequest::try_from(&connector_router_data)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) + } + fn build_request( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCompleteAuthorizeRouterData, + res: types::Response, + ) -> CustomResult { + let response: nmi::NmiCompleteResponse = serde_urlencoded::from_bytes(&res.response) + .into_report() + .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 for Nmi { @@ -300,14 +471,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nmi::NmiSyncRequest::try_from(req)?; - let nmi_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -320,7 +486,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = nmi::NmiRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -378,12 +544,7 @@ impl ConnectorIntegration::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -398,7 +559,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nmi::NmiCancelRequest::try_from(req)?; - let nmi_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -471,7 +627,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = nmi::NmiRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -530,12 +686,7 @@ impl ConnectorIntegration::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -550,7 +701,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nmi::NmiSyncRequest::try_from(req)?; - let nmi_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -621,7 +767,7 @@ impl ConnectorIntegration, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let sig_header = + connector_utils::get_header_key_value("webhook-signature", request.headers)?; + + let regex_pattern = r"t=(.*),s=(.*)"; + + if let Some(captures) = Regex::new(regex_pattern) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound)? + .captures(sig_header) + { + let signature = captures + .get(1) + .ok_or(errors::ConnectorError::WebhookSignatureNotFound) + .into_report()? + .as_str(); + return Ok(signature.as_bytes().to_vec()); + } + + Err(errors::ConnectorError::WebhookSignatureNotFound).into_report() + } + + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let sig_header = + connector_utils::get_header_key_value("webhook-signature", request.headers)?; + + let regex_pattern = r"t=(.*),s=(.*)"; + + if let Some(captures) = Regex::new(regex_pattern) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound)? + .captures(sig_header) + { + let nonce = captures + .get(0) + .ok_or(errors::ConnectorError::WebhookSignatureNotFound) + .into_report()? + .as_str(); + + let message = format!("{}.{}", nonce, String::from_utf8_lossy(request.body)); + + return Ok(message.into_bytes()); + } + Err(errors::ConnectorError::WebhookSignatureNotFound).into_report() + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let reference_body: nmi::NmiWebhookObjectReference = request + .body + .parse_struct("nmi NmiWebhookObjectReference") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + let object_reference_id = match reference_body.event_body.action.action_type { + nmi::NmiActionType::Sale => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + reference_body.event_body.order_id, + ), + ), + nmi::NmiActionType::Auth => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + reference_body.event_body.order_id, + ), + ), + nmi::NmiActionType::Capture => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + reference_body.event_body.order_id, + ), + ), + nmi::NmiActionType::Void => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + reference_body.event_body.order_id, + ), + ), + nmi::NmiActionType::Refund => api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::RefundId(reference_body.event_body.order_id), + ), + _ => Err(errors::ConnectorError::WebhooksNotImplemented).into_report()?, + }; + + Ok(object_reference_id) } fn get_webhook_event_type( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Ok(api::IncomingWebhookEvent::EventNotSupported) + let event_type_body: nmi::NmiWebhookEventBody = request + .body + .parse_struct("nmi NmiWebhookEventType") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + Ok(api::IncomingWebhookEvent::foreign_from( + event_type_body.event_type, + )) } fn get_webhook_resource_object( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + let webhook_body: nmi::NmiWebhookBody = request + .body + .parse_struct("nmi NmiWebhookBody") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + match webhook_body.event_body.action.action_type { + nmi::NmiActionType::Sale + | nmi::NmiActionType::Auth + | nmi::NmiActionType::Capture + | nmi::NmiActionType::Void + | nmi::NmiActionType::Credit => { + Ok(Box::new(nmi::SyncResponse::try_from(&webhook_body)?)) + } + nmi::NmiActionType::Refund => Ok(Box::new(webhook_body)), + } } } diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 6e887f58858f..5b486aae600c 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -1,12 +1,17 @@ +use api_models::webhooks; use cards::CardNumber; -use common_utils::ext_traits::XmlExt; +use common_utils::{errors::CustomResult, ext_traits::XmlExt}; use error_stack::{IntoReport, Report, ResultExt}; -use masking::Secret; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, PaymentsAuthorizeRequestData}, + connector::utils::{ + self, AddressDetailsData, PaymentsAuthorizeRequestData, + PaymentsCompleteAuthorizeRequestData, RouterData, + }, core::errors, + services, types::{self, api, storage::enums, transformers::ForeignFrom, ConnectorAuthType}, }; @@ -25,17 +30,22 @@ pub enum TransactionType { pub struct NmiAuthType { pub(super) api_key: Secret, + pub(super) public_key: Option>, } impl TryFrom<&ConnectorAuthType> for NmiAuthType { type Error = Error; fn try_from(auth_type: &ConnectorAuthType) -> Result { - if let types::ConnectorAuthType::HeaderKey { api_key } = auth_type { - Ok(Self { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { api_key: api_key.to_owned(), - }) - } else { - Err(errors::ConnectorError::FailedToObtainAuthType.into()) + public_key: None, + }), + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + api_key: api_key.to_owned(), + public_key: Some(key1.to_owned()), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } @@ -71,6 +81,319 @@ impl } } +#[derive(Debug, Serialize)] +pub struct NmiVaultRequest { + security_key: Secret, + ccnumber: CardNumber, + ccexp: Secret, + cvv: Secret, + first_name: Secret, + last_name: Secret, + customer_vault: CustomerAction, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum CustomerAction { + AddCustomer, + UpdateCustomer, +} + +impl TryFrom<&types::PaymentsPreProcessingRouterData> for NmiVaultRequest { + type Error = Error; + fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result { + let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?; + let (ccnumber, ccexp, cvv) = get_card_details(item.request.payment_method_data.clone())?; + let billing_details = item.get_billing_address()?; + + Ok(Self { + security_key: auth_type.api_key, + ccnumber, + ccexp, + cvv, + first_name: billing_details.get_first_name()?.to_owned(), + last_name: billing_details.get_last_name()?.to_owned(), + customer_vault: CustomerAction::AddCustomer, + }) + } +} + +fn get_card_details( + payment_method_data: Option, +) -> CustomResult<(CardNumber, Secret, Secret), errors::ConnectorError> { + match payment_method_data { + Some(api::PaymentMethodData::Card(ref card_details)) => Ok(( + card_details.card_number.clone(), + utils::CardData::get_card_expiry_month_year_2_digit_with_delimiter( + card_details, + "".to_string(), + )?, + card_details.card_cvc.clone(), + )), + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Nmi"), + )) + .into_report(), + } +} + +#[derive(Debug, Deserialize)] +pub struct NmiVaultResponse { + pub response: Response, + pub responsetext: String, + pub customer_vault_id: Option, + pub response_code: String, + pub transactionid: String, +} + +impl + TryFrom< + types::ResponseRouterData< + api::PreProcessing, + NmiVaultResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + > for types::PaymentsPreProcessingRouterData +{ + type Error = Error; + fn try_from( + item: types::ResponseRouterData< + api::PreProcessing, + NmiVaultResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + ) -> Result { + let auth_type: NmiAuthType = (&item.data.connector_auth_type).try_into()?; + let amount_data = + item.data + .request + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?; + let currency_data = + item.data + .request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?; + let (response, status) = match item.response.response { + Response::Approved => ( + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: Some(services::RedirectForm::Nmi { + amount: utils::to_currency_base_unit_asf64( + amount_data, + currency_data.to_owned(), + )? + .to_string(), + currency: currency_data, + customer_vault_id: item.response.customer_vault_id.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "customer_vault_id", + }, + )?, + public_key: auth_type.public_key.ok_or( + errors::ConnectorError::InvalidConnectorConfig { + config: "public_key", + }, + )?, + order_id: item.data.connector_request_reference_id.clone(), + }), + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transactionid), + incremental_authorization_allowed: None, + }), + enums::AttemptStatus::AuthenticationPending, + ), + Response::Declined | Response::Error => ( + Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.responsetext.to_owned(), + reason: Some(item.response.responsetext), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(item.response.transactionid), + }), + enums::AttemptStatus::Failure, + ), + }; + Ok(Self { + status, + response, + ..item.data + }) + } +} + +#[derive(Debug, Serialize)] +pub struct NmiCompleteRequest { + amount: f64, + #[serde(rename = "type")] + transaction_type: TransactionType, + security_key: Secret, + orderid: String, + ccnumber: CardNumber, + ccexp: Secret, + cardholder_auth: CardHolderAuthType, + cavv: String, + xid: String, + three_ds_version: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CardHolderAuthType { + Verified, + Attempted, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum ThreeDsVersion { + #[serde(rename = "2.0.0")] + VersionTwo, + #[serde(rename = "2.1.0")] + VersionTwoPointOne, + #[serde(rename = "2.2.0")] + VersionTwoPointTwo, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NmiRedirectResponseData { + cavv: String, + xid: String, + card_holder_auth: CardHolderAuthType, + three_ds_version: Option, + order_id: String, +} + +impl TryFrom<&NmiRouterData<&types::PaymentsCompleteAuthorizeRouterData>> for NmiCompleteRequest { + type Error = Error; + fn try_from( + item: &NmiRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + ) -> Result { + let transaction_type = match item.router_data.request.is_auto_capture()? { + true => TransactionType::Sale, + false => TransactionType::Auth, + }; + let auth_type: NmiAuthType = (&item.router_data.connector_auth_type).try_into()?; + let payload_data = item + .router_data + .request + .get_redirect_response_payload()? + .expose(); + + let three_ds_data: NmiRedirectResponseData = serde_json::from_value(payload_data) + .into_report() + .change_context(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "three_ds_data", + })?; + + let (ccnumber, ccexp, ..) = + get_card_details(item.router_data.request.payment_method_data.clone())?; + + Ok(Self { + amount: item.amount, + transaction_type, + security_key: auth_type.api_key, + orderid: three_ds_data.order_id, + ccnumber, + ccexp, + cardholder_auth: three_ds_data.card_holder_auth, + cavv: three_ds_data.cavv, + xid: three_ds_data.xid, + three_ds_version: three_ds_data.three_ds_version, + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct NmiCompleteResponse { + pub response: Response, + pub responsetext: String, + pub authcode: Option, + pub transactionid: String, + pub avsresponse: Option, + pub cvvresponse: Option, + pub orderid: String, + pub response_code: String, +} + +impl + TryFrom< + types::ResponseRouterData< + api::CompleteAuthorize, + NmiCompleteResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::PaymentsCompleteAuthorizeRouterData +{ + type Error = Error; + fn try_from( + item: types::ResponseRouterData< + api::CompleteAuthorize, + NmiCompleteResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + let (response, status) = match item.response.response { + Response::Approved => ( + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transactionid, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.orderid), + incremental_authorization_allowed: None, + }), + if let Some(diesel_models::enums::CaptureMethod::Automatic) = + item.data.request.capture_method + { + enums::AttemptStatus::CaptureInitiated + } else { + enums::AttemptStatus::Authorizing + }, + ), + Response::Declined | Response::Error => ( + Err(types::ErrorResponse::foreign_from(( + item.response, + item.http_code, + ))), + enums::AttemptStatus::Failure, + ), + }; + Ok(Self { + status, + response, + ..item.data + }) + } +} + +impl ForeignFrom<(NmiCompleteResponse, u16)> for types::ErrorResponse { + fn foreign_from((response, http_code): (NmiCompleteResponse, u16)) -> Self { + Self { + code: response.response_code, + message: response.responsetext.to_owned(), + reason: Some(response.responsetext), + status_code: http_code, + attempt_status: None, + connector_transaction_id: Some(response.transactionid), + } + } +} + #[derive(Debug, Serialize)] pub struct NmiPaymentsRequest { #[serde(rename = "type")] @@ -139,7 +462,7 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { payment_method_data: &api_models::payments::PaymentMethodData, ) -> Result { match &payment_method_data { - api::PaymentMethodData::Card(ref card) => Ok(Self::from(card)), + api::PaymentMethodData::Card(ref card) => Ok(Self::try_from(card)?), api::PaymentMethodData::Wallet(ref wallet_type) => match wallet_type { api_models::payments::WalletData::GooglePay(ref googlepay_data) => { Ok(Self::from(googlepay_data)) @@ -171,10 +494,9 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { | api_models::payments::WalletData::WeChatPayQr(_) | api_models::payments::WalletData::CashappQr(_) | api_models::payments::WalletData::SwishQr(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "nmi", - }) + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("nmi"), + )) .into_report() } }, @@ -188,27 +510,28 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "nmi", - }) + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("nmi"), + )) .into_report(), } } } -impl From<&api_models::payments::Card> for PaymentMethod { - fn from(card: &api_models::payments::Card) -> Self { +impl TryFrom<&api_models::payments::Card> for PaymentMethod { + type Error = Error; + fn try_from(card: &api_models::payments::Card) -> Result { let ccexp = utils::CardData::get_card_expiry_month_year_2_digit_with_delimiter( card, "".to_string(), - ); + )?; let card = CardData { ccnumber: card.card_number.clone(), ccexp, cvv: card.card_cvc.clone(), }; - Self::Card(Box::new(card)) + Ok(Self::Card(Box::new(card))) } } @@ -248,7 +571,7 @@ impl TryFrom<&types::SetupMandateRouterData> for NmiPaymentsRequest { #[derive(Debug, Serialize)] pub struct NmiSyncRequest { - pub transaction_id: String, + pub order_id: String, pub security_key: Secret, } @@ -258,11 +581,7 @@ impl TryFrom<&types::PaymentsSyncRouterData> for NmiSyncRequest { let auth = NmiAuthType::try_from(&item.connector_auth_type)?; Ok(Self { security_key: auth.api_key, - transaction_id: item - .request - .connector_transaction_id - .get_connector_transaction_id() - .change_context(errors::ConnectorError::MissingConnectorTransactionID)?, + order_id: item.attempt_id.clone(), }) } } @@ -314,13 +633,14 @@ impl Response::Approved => ( Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transactionid, + item.response.transactionid.to_owned(), ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.orderid), + incremental_authorization_allowed: None, }), enums::AttemptStatus::CaptureInitiated, ), @@ -407,13 +727,14 @@ impl Response::Approved => ( Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transactionid, + item.response.transactionid.to_owned(), ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.orderid), + incremental_authorization_allowed: None, }), enums::AttemptStatus::Charged, ), @@ -437,10 +758,11 @@ impl ForeignFrom<(StandardResponse, u16)> for types::ErrorResponse { fn foreign_from((response, http_code): (StandardResponse, u16)) -> Self { Self { code: response.response_code, - message: response.responsetext, - reason: None, + message: response.responsetext.to_owned(), + reason: Some(response.responsetext), status_code: http_code, attempt_status: None, + connector_transaction_id: Some(response.transactionid), } } } @@ -461,13 +783,14 @@ impl TryFrom> Response::Approved => ( Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transactionid, + item.response.transactionid.to_owned(), ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.orderid), + incremental_authorization_allowed: None, }), if let Some(diesel_models::enums::CaptureMethod::Automatic) = item.data.request.capture_method @@ -510,13 +833,14 @@ impl Response::Approved => ( Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transactionid, + item.response.transactionid.to_owned(), ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.orderid), + incremental_authorization_allowed: None, }), enums::AttemptStatus::VoidInitiated, ), @@ -568,6 +892,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -587,6 +912,19 @@ impl TryFrom> for SyncResponse { } } +impl TryFrom> for NmiRefundSyncResponse { + type Error = Error; + fn try_from(bytes: Vec) -> Result { + let query_response = String::from_utf8(bytes) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + query_response + .parse_xml::() + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + } +} + impl From for enums::AttemptStatus { fn from(item: NmiStatus) -> Self { match item { @@ -607,6 +945,7 @@ pub struct NmiRefundRequest { transaction_type: TransactionType, security_key: Secret, transactionid: String, + orderid: String, amount: f64, } @@ -618,6 +957,7 @@ impl TryFrom<&NmiRouterData<&types::RefundsRouterData>> for NmiRefundReque transaction_type: TransactionType::Refund, security_key: auth_type.api_key, transactionid: item.router_data.request.connector_transaction_id.clone(), + orderid: item.router_data.request.refund_id.clone(), amount: item.amount, }) } @@ -633,7 +973,7 @@ impl TryFrom> let refund_status = enums::RefundStatus::from(item.response.response); Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.transactionid, + connector_refund_id: item.response.orderid, refund_status, }), ..item.data @@ -672,15 +1012,14 @@ impl TryFrom<&types::RefundSyncRouterData> for NmiSyncRequest { type Error = Error; fn try_from(item: &types::RefundSyncRouterData) -> Result { let auth = NmiAuthType::try_from(&item.connector_auth_type)?; - let transaction_id = item - .request - .connector_refund_id - .clone() - .ok_or(errors::ConnectorError::MissingConnectorRefundID)?; Ok(Self { security_key: auth.api_key, - transaction_id, + order_id: item + .request + .connector_refund_id + .clone() + .ok_or(errors::ConnectorError::MissingConnectorRefundID)?, }) } } @@ -692,12 +1031,12 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - let response = SyncResponse::try_from(item.response.response.to_vec())?; + let response = NmiRefundSyncResponse::try_from(item.response.response.to_vec())?; let refund_status = enums::RefundStatus::from(NmiStatus::from(response.transaction.condition)); Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: response.transaction.transaction_id, + connector_refund_id: response.transaction.order_id, refund_status, }), ..item.data @@ -734,13 +1073,137 @@ impl From for NmiStatus { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct SyncTransactionResponse { - transaction_id: String, + pub transaction_id: String, + pub condition: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SyncResponse { + pub transaction: SyncTransactionResponse, +} + +#[derive(Debug, Deserialize)] +pub struct RefundSyncBody { + order_id: String, condition: String, } #[derive(Debug, Deserialize)] -struct SyncResponse { - transaction: SyncTransactionResponse, +struct NmiRefundSyncResponse { + transaction: RefundSyncBody, +} + +#[derive(Debug, Deserialize)] +pub struct NmiWebhookObjectReference { + pub event_body: NmiReferenceBody, +} + +#[derive(Debug, Deserialize)] +pub struct NmiReferenceBody { + pub order_id: String, + pub action: NmiActionBody, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NmiActionBody { + pub action_type: NmiActionType, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum NmiActionType { + Auth, + Capture, + Credit, + Refund, + Sale, + Void, +} + +#[derive(Debug, Deserialize)] +pub struct NmiWebhookEventBody { + pub event_type: NmiWebhookEventType, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum NmiWebhookEventType { + #[serde(rename = "transaction.sale.success")] + SaleSuccess, + #[serde(rename = "transaction.sale.failure")] + SaleFailure, + #[serde(rename = "transaction.sale.unknown")] + SaleUnknown, + #[serde(rename = "transaction.auth.success")] + AuthSuccess, + #[serde(rename = "transaction.auth.failure")] + AuthFailure, + #[serde(rename = "transaction.auth.unknown")] + AuthUnknown, + #[serde(rename = "transaction.refund.success")] + RefundSuccess, + #[serde(rename = "transaction.refund.failure")] + RefundFailure, + #[serde(rename = "transaction.refund.unknown")] + RefundUnknown, + #[serde(rename = "transaction.void.success")] + VoidSuccess, + #[serde(rename = "transaction.void.failure")] + VoidFailure, + #[serde(rename = "transaction.void.unknown")] + VoidUnknown, + #[serde(rename = "transaction.capture.success")] + CaptureSuccess, + #[serde(rename = "transaction.capture.failure")] + CaptureFailure, + #[serde(rename = "transaction.capture.unknown")] + CaptureUnknown, +} + +impl ForeignFrom for webhooks::IncomingWebhookEvent { + fn foreign_from(status: NmiWebhookEventType) -> Self { + match status { + NmiWebhookEventType::SaleSuccess => Self::PaymentIntentSuccess, + NmiWebhookEventType::SaleFailure => Self::PaymentIntentFailure, + NmiWebhookEventType::RefundSuccess => Self::RefundSuccess, + NmiWebhookEventType::RefundFailure => Self::RefundFailure, + NmiWebhookEventType::VoidSuccess => Self::PaymentIntentCancelled, + NmiWebhookEventType::AuthSuccess => Self::PaymentIntentAuthorizationSuccess, + NmiWebhookEventType::CaptureSuccess => Self::PaymentIntentCaptureSuccess, + NmiWebhookEventType::AuthFailure => Self::PaymentIntentAuthorizationFailure, + NmiWebhookEventType::CaptureFailure => Self::PaymentIntentCaptureFailure, + NmiWebhookEventType::VoidFailure => Self::PaymentIntentCancelFailure, + NmiWebhookEventType::SaleUnknown + | NmiWebhookEventType::RefundUnknown + | NmiWebhookEventType::AuthUnknown + | NmiWebhookEventType::VoidUnknown + | NmiWebhookEventType::CaptureUnknown => Self::EventNotSupported, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NmiWebhookBody { + pub event_body: NmiWebhookObject, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NmiWebhookObject { + pub transaction_id: String, + pub order_id: String, + pub condition: String, + pub action: NmiActionBody, +} + +impl TryFrom<&NmiWebhookBody> for SyncResponse { + type Error = Error; + fn try_from(item: &NmiWebhookBody) -> Result { + let transaction = SyncTransactionResponse { + transaction_id: item.event_body.transaction_id.to_owned(), + condition: item.event_body.condition.to_owned(), + }; + + Ok(Self { transaction }) + } } diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 0ea73efd94bd..6c98a3076375 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -3,7 +3,7 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; -use common_utils::{crypto, ext_traits::ByteSliceExt}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; @@ -28,7 +28,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -46,6 +46,7 @@ impl api::Refund for Noon {} impl api::RefundExecute for Noon {} impl api::RefundSync for Noon {} impl api::PaymentToken for Noon {} +impl api::ConnectorMandateRevoke for Noon {} impl ConnectorIntegration< @@ -137,6 +138,7 @@ impl ConnectorCommon for Noon { message: response.class_description, reason: Some(response.message), attempt_status: None, + connector_transaction_id: None, }) } } @@ -182,6 +184,20 @@ impl types::PaymentsResponseData, > for Noon { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Noon".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -211,14 +227,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = noon::NoonPaymentsRequest::try_from(req)?; - let noon_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(noon_req)) + ) -> CustomResult { + let connector_req = noon::NoonPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -236,7 +247,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = noon::NoonPaymentsActionRequest::try_from(req)?; - let noon_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(noon_req)) + ) -> CustomResult { + let connector_req = noon::NoonPaymentsActionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -383,7 +389,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = noon::NoonPaymentsCancelRequest::try_from(req)?; - let noon_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(noon_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -461,7 +462,7 @@ impl ConnectorIntegration for Noon +{ + fn get_headers( + &self, + req: &types::MandateRevokeRouterData, + 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::MandateRevokeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}payment/v1/order", self.base_url(connectors))) + } + fn build_request( + &self, + req: &types::MandateRevokeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::MandateRevokeType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::MandateRevokeType::get_headers( + self, req, connectors, + )?) + .set_body(types::MandateRevokeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + fn get_request_body( + &self, + req: &types::MandateRevokeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = noon::NoonRevokeMandateRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn handle_response( + &self, + data: &types::MandateRevokeRouterData, + res: Response, + ) -> CustomResult { + let response: noon::NoonRevokeMandateResponse = res + .response + .parse_struct("Noon NoonRevokeMandateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration for Noon { fn get_headers( &self, @@ -517,14 +593,9 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = noon::NoonPaymentsActionRequest::try_from(req)?; - let noon_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(noon_req)) + ) -> CustomResult { + let connector_req = noon::NoonPaymentsActionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -539,7 +610,7 @@ impl ConnectorIntegration, - ) -> 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 2ec879252d44..8bb3a96a3cae 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -5,7 +5,8 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{ - self as conn_utils, CardData, PaymentsAuthorizeRequestData, RouterData, WalletData, + self as conn_utils, CardData, PaymentsAuthorizeRequestData, RevokeMandateRequestData, + RouterData, WalletData, }, core::errors, services, @@ -30,11 +31,13 @@ pub enum NoonSubscriptionType { } #[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct NoonSubscriptionData { #[serde(rename = "type")] subscription_type: NoonSubscriptionType, //Short description about the subscription. name: String, + max_amount: Option, } #[derive(Debug, Serialize)] @@ -168,12 +171,13 @@ pub enum NoonPaymentData { } #[derive(Debug, Serialize)] -#[serde(rename_all = "UPPERCASE")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum NoonApiOperations { Initiate, Capture, Reverse, Refund, + CancelSubscription, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -200,7 +204,10 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { _ => ( match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(req_card) => Ok(NoonPaymentData::Card(NoonCard { - name_on_card: req_card.card_holder_name.clone(), + name_on_card: req_card + .card_holder_name + .clone() + .unwrap_or(Secret::new("".to_string())), number_plain: req_card.card_number.clone(), expiry_month: req_card.card_exp_month.clone(), expiry_year: req_card.get_expiry_year_4_digit(), @@ -283,7 +290,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::NotImplemented( conn_utils::get_unimplemented_payment_method_error_message("Noon"), )) @@ -329,6 +337,21 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { NoonSubscriptionData { subscription_type: NoonSubscriptionType::Unscheduled, name: name.clone(), + max_amount: item + .request + .setup_mandate_details + .clone() + .and_then(|mandate_details| match mandate_details.mandate_type { + Some(data_models::mandates::MandateDataType::SingleUse(mandate)) + | Some(data_models::mandates::MandateDataType::MultiUse(Some( + mandate, + ))) => Some( + conn_utils::to_currency_base_unit(mandate.amount, mandate.currency) + .ok(), + ), + _ => None, + }) + .flatten(), }, true, )) { @@ -444,7 +467,7 @@ impl ForeignFrom<(NoonPaymentStatus, Self)> for enums::AttemptStatus { } #[derive(Debug, Serialize, Deserialize)] -pub struct NoonSubscriptionResponse { +pub struct NoonSubscriptionObject { identifier: String, } @@ -469,7 +492,7 @@ pub struct NoonCheckoutData { pub struct NoonPaymentsResponseResult { order: NoonPaymentsOrderResponse, checkout_data: Option, - subscription: Option, + subscription: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -510,6 +533,7 @@ impl reason: Some(error_message), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), _ => { let connector_response_reference_id = @@ -523,6 +547,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id, + incremental_authorization_allowed: None, }) } }, @@ -595,6 +620,25 @@ impl TryFrom<&types::PaymentsCancelRouterData> for NoonPaymentsCancelRequest { } } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NoonRevokeMandateRequest { + api_operation: NoonApiOperations, + subscription: NoonSubscriptionObject, +} + +impl TryFrom<&types::MandateRevokeRouterData> for NoonRevokeMandateRequest { + type Error = error_stack::Report; + fn try_from(item: &types::MandateRevokeRouterData) -> Result { + Ok(Self { + api_operation: NoonApiOperations::CancelSubscription, + subscription: NoonSubscriptionObject { + identifier: item.request.get_connector_mandate_id()?, + }, + }) + } +} + impl TryFrom<&types::RefundsRouterData> for NoonPaymentsActionRequest { type Error = error_stack::Report; fn try_from(item: &types::RefundsRouterData) -> Result { @@ -616,6 +660,55 @@ impl TryFrom<&types::RefundsRouterData> for NoonPaymentsActionRequest { }) } } +#[derive(Debug, Deserialize)] +pub enum NoonRevokeStatus { + Cancelled, +} + +#[derive(Debug, Deserialize)] +pub struct NoonCancelSubscriptionObject { + status: NoonRevokeStatus, +} + +#[derive(Debug, Deserialize)] +pub struct NoonRevokeMandateResult { + subscription: NoonCancelSubscriptionObject, +} + +#[derive(Debug, Deserialize)] +pub struct NoonRevokeMandateResponse { + result: NoonRevokeMandateResult, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + NoonRevokeMandateResponse, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + NoonRevokeMandateResponse, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + >, + ) -> Result { + match item.response.result.subscription.status { + NoonRevokeStatus::Cancelled => Ok(Self { + response: Ok(types::MandateRevokeResponseData { + mandate_status: common_enums::MandateStatus::Revoked, + }), + ..item.data + }), + } + } +} #[derive(Debug, Default, Deserialize, Clone)] #[serde(rename_all = "UPPERCASE")] diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs index 15702829d378..35de293443e8 100644 --- a/crates/router/src/connector/nuvei.rs +++ b/crates/router/src/connector/nuvei.rs @@ -6,6 +6,7 @@ use ::common_utils::{ crypto, errors::ReportSwitchExt, ext_traits::{BytesExt, ValueExt}, + request::RequestContent, }; use error_stack::{IntoReport, ResultExt}; use transformers as nuvei; @@ -25,7 +26,7 @@ use crate::{ storage::enums, ErrorResponse, Response, }, - utils::{self as common_utils, ByteSliceExt, Encode}, + utils::ByteSliceExt, }; #[derive(Debug, Clone)] @@ -117,6 +118,20 @@ impl types::PaymentsResponseData, > for Nuvei { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Nuvei".to_string()) + .into(), + ) + } } impl @@ -150,16 +165,11 @@ impl &self, req: &types::PaymentsCompleteAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let meta: nuvei::NuveiMeta = utils::to_connector_meta(req.request.connector_meta.clone())?; - let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, meta.session_token))?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, meta.session_token))?; - Ok(Some(req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -176,7 +186,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -237,14 +247,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentFlowRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -257,7 +262,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentSyncRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -344,7 +344,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentFlowRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -429,7 +424,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; - Ok(Some(req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -592,7 +582,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiSessionRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = nuvei::NuveiSessionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -683,7 +668,7 @@ impl .headers(types::PaymentsPreAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsPreAuthorizeType::get_request_body( + .set_body(types::PaymentsPreAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -742,15 +727,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; - Ok(Some(req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -764,7 +744,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentFlowRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -846,7 +821,7 @@ impl ConnectorIntegration, - ) -> 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..4ed6b25b136c 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"), ) @@ -1002,7 +1000,7 @@ impl From for PaymentOption { Self { card: Some(Card { card_number: Some(card.card_number), - card_holder_name: Some(card.card_holder_name), + card_holder_name: card.card_holder_name, expiration_month: Some(card.card_exp_month), expiration_year: Some(card.card_exp_year), three_d: card_details.three_d, @@ -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..0ab5785a445f 100644 --- a/crates/router/src/connector/opayo.rs +++ b/crates/router/src/connector/opayo.rs @@ -2,6 +2,7 @@ mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::ExposeInterface; @@ -22,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -108,6 +109,7 @@ impl ConnectorCommon for Opayo { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -145,6 +147,20 @@ impl types::PaymentsResponseData, > for Opayo { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Opayo".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -174,14 +190,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = opayo::OpayoPaymentsRequest::try_from(req)?; - let opayo_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(opayo_req)) + ) -> CustomResult { + let connector_req = opayo::OpayoPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -199,7 +210,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -336,7 +347,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = opayo::OpayoRefundRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = opayo::OpayoRefundRequest::try_from(req)?; - let opayo_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(opayo_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -420,7 +426,7 @@ impl ConnectorIntegration, - ) -> 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..94ab62b4f7f1 100644 --- a/crates/router/src/connector/opayo/transformers.rs +++ b/crates/router/src/connector/opayo/transformers.rs @@ -29,7 +29,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for OpayoPaymentsRequest { match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(req_card) => { let card = OpayoCard { - name: req_card.card_holder_name, + name: req_card + .card_holder_name + .unwrap_or(Secret::new("".to_string())), number: req_card.card_number, expiry_month: req_card.card_exp_month, expiry_year: req_card.card_exp_year, @@ -52,7 +54,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 +125,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..1eafa67fddae 100644 --- a/crates/router/src/connector/opennode.rs +++ b/crates/router/src/connector/opennode.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::crypto; +use common_utils::{crypto, request::RequestContent}; use error_stack::{IntoReport, ResultExt}; use transformers as opennode; @@ -22,7 +22,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{BytesExt, Encode}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -111,6 +111,7 @@ impl ConnectorCommon for Opennode { message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -145,6 +146,20 @@ impl types::PaymentsResponseData, > for Opennode { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Opennode".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -174,20 +189,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = opennode::OpennodeRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = opennode::OpennodePaymentsRequest::try_from(&connector_router_data)?; - let opennode_req = types::RequestBody::log_and_get_request_body( - &req_obj, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(opennode_req)) + let connector_req = opennode::OpennodePaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -204,7 +214,7 @@ impl ConnectorIntegration, - ) -> 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..442bbaca3128 100644 --- a/crates/router/src/connector/payeezy.rs +++ b/crates/router/src/connector/payeezy.rs @@ -3,6 +3,7 @@ mod transformers; use std::fmt::Debug; use base64::Engine; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::ExposeInterface; @@ -26,7 +27,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -42,10 +43,8 @@ where connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { let auth = payeezy::PayeezyAuthType::try_from(&req.connector_auth_type)?; - let option_request_payload = self.get_request_body(req, connectors)?; - let request_payload = option_request_payload.map_or("{}".to_string(), |payload| { - types::RequestBody::get_inner_value(payload).expose() - }); + let request_payload = + types::RequestBody::get_inner_value(self.get_request_body(req, connectors)?).expose(); let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .ok() @@ -124,6 +123,7 @@ impl ConnectorCommon for Payeezy { message: error_messages.join(", "), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -153,6 +153,20 @@ impl types::PaymentsResponseData, > for Payeezy { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Payeezy".to_string()) + .into(), + ) + } } impl api::PaymentToken for Payeezy {} @@ -201,14 +215,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = payeezy::PayeezyCaptureOrVoidRequest::try_from(req)?; - let payeezy_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payeezy_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -221,7 +230,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = payeezy::PayeezyRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let req_obj = payeezy::PayeezyCaptureOrVoidRequest::try_from(&router_obj)?; - let payeezy_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let connector_req = payeezy::PayeezyCaptureOrVoidRequest::try_from(&router_obj)?; - Ok(Some(payeezy_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -329,7 +333,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = payeezy::PayeezyRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = payeezy::PayeezyPaymentsRequest::try_from(&router_obj)?; + let connector_req = payeezy::PayeezyPaymentsRequest::try_from(&router_obj)?; - let payeezy_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payeezy_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -429,7 +428,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = payeezy::PayeezyRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = payeezy::PayeezyRefundRequest::try_from(&router_obj)?; - let payeezy_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payeezy_req)) + let connector_req = payeezy::PayeezyRefundRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -524,7 +518,7 @@ impl ConnectorIntegration, - ) -> 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 e2e837929c41..c633fd3d99f9 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -72,11 +72,9 @@ impl TryFrom for PayeezyCardType { utils::CardIssuer::Maestro | utils::CardIssuer::DinersClub | utils::CardIssuer::JCB - | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Payeezy", - } - .into()), + | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Payeezy"), + ))?, } } } @@ -243,9 +241,12 @@ fn get_payment_method_data( let card_type = PayeezyCardType::try_from(card.get_card_issuer()?)?; let payeezy_card = PayeezyCard { card_type, - cardholder_name: card.card_holder_name.clone(), + cardholder_name: card + .card_holder_name + .clone() + .unwrap_or(Secret::new("".to_string())), card_number: card.card_number.clone(), - exp_date: card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + exp_date: card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, cvv: card.card_cvc.clone(), }; Ok(PayeezyPaymentMethod::PayeezyCard(payeezy_card)) @@ -262,11 +263,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"), + ))?, } } @@ -443,6 +443,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..40f4c646e507 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -3,7 +3,7 @@ pub mod transformers; use std::fmt::Debug; use api_models::enums::AuthenticationType; -use common_utils::crypto; +use common_utils::{crypto, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::ExposeInterface; @@ -23,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, domain, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -98,6 +98,7 @@ impl ConnectorCommon for Payme { response.status_error_details, response.status_additional_info )), attempt_status: None, + connector_transaction_id: None, }) } } @@ -151,15 +152,10 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = payme::CaptureBuyerRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = payme::CaptureBuyerRequest::try_from(req)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -174,7 +170,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -257,18 +253,13 @@ impl &self, req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let amount = req.request.get_amount()?; let currency = req.request.get_currency()?; let connector_router_data = payme::PaymeRouterData::try_from((&self.get_currency_unit(), currency, amount, req))?; - let req_obj = payme::GenerateSaleRequest::try_from(&connector_router_data)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + let connector_req = payme::GenerateSaleRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -286,7 +277,7 @@ impl .url(&types::PaymentsPreProcessingType::get_url( self, req, connectors, )?) - .body(types::PaymentsPreProcessingType::get_request_body( + .set_body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -338,6 +329,20 @@ impl types::PaymentsResponseData, > for Payme { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Payme".to_string()) + .into(), + ) + } } impl services::ConnectorRedirectResponse for Payme { @@ -383,14 +388,9 @@ impl &self, req: &types::PaymentsCompleteAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = payme::Pay3dsRequest::try_from(req)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + ) -> CustomResult { + let connector_req = payme::Pay3dsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -407,7 +407,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -486,20 +486,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = payme::PaymePaymentRequest::try_from(&connector_router_data)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + let connector_req = payme::PaymePaymentRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -517,7 +512,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = payme::PaymeQuerySaleRequest::try_from(req)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + ) -> CustomResult { + let connector_req = payme::PaymeQuerySaleRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -604,7 +594,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let req_obj = payme::PaymentCaptureRequest::try_from(&connector_router_data)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + let connector_req = payme::PaymentCaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -700,7 +685,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = payme::PaymeRefundRequest::try_from(&connector_router_data)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + let connector_req = payme::PaymeRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -808,7 +788,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = payme::PaymeQueryTransactionRequest::try_from(req)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + ) -> CustomResult { + let connector_req = payme::PaymeQueryTransactionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -892,7 +867,7 @@ impl ConnectorIntegration, - ) -> 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..a7b21ce292de 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 }), @@ -539,6 +545,13 @@ impl } _ => { let currency_code = item.data.request.get_currency()?; + let country_code = item + .data + .address + .billing + .as_ref() + .and_then(|billing| billing.address.as_ref()) + .and_then(|address| address.country); let amount = item.data.request.get_amount()?; let amount_in_base_unit = utils::to_currency_base_unit(amount, currency_code)?; let pmd = item.data.request.payment_method_data.to_owned(); @@ -553,7 +566,7 @@ impl api_models::payments::ApplePaySessionResponse::NoSessionResponse, payment_request_data: Some( api_models::payments::ApplePayPaymentRequest { - country_code: item.data.get_billing_country()?, + country_code, currency_code, total: api_models::payments::AmountInfo { label: "Apple Pay".to_string(), @@ -635,7 +648,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayRequest { let card = PaymeCard { credit_card_cvv: req_card.card_cvc.clone(), credit_card_exp: req_card - .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, credit_card_number: req_card.card_number, }; let buyer_email = item.request.get_email()?; @@ -664,7 +677,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 +737,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()) } @@ -740,7 +755,7 @@ impl TryFrom<&types::TokenizationRouterData> for CaptureBuyerRequest { let card = PaymeCard { credit_card_cvv: req_card.card_cvc.clone(), credit_card_exp: req_card - .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, credit_card_number: req_card.card_number, }; Ok(Self { @@ -759,7 +774,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..742c563407c5 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -2,13 +2,13 @@ pub mod transformers; use std::fmt::{Debug, Write}; use base64::Engine; -use common_utils::ext_traits::ByteSliceExt; +use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; 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,10 +30,11 @@ 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}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -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, }) } } @@ -222,10 +265,6 @@ impl ConnectorValidation for Paypal { ), } } - - fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { - Ok(()) - } } impl @@ -260,15 +299,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![ ( @@ -284,15 +317,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = paypal::PaypalAuthUpdateRequest::try_from(req)?; - let paypal_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = paypal::PaypalAuthUpdateRequest::try_from(req)?; - Ok(Some(paypal_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -305,7 +333,7 @@ impl ConnectorIntegration for Paypal { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Paypal".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -386,23 +429,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), req.request.currency, - req.request - .surcharge_details - .as_ref() - .map_or(req.request.amount, |surcharge| surcharge.final_amount), + req.request.amount, req, ))?; - let req_obj = paypal::PaypalPaymentsRequest::try_from(&connector_router_data)?; - let paypal_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(paypal_req)) + let connector_req = paypal::PaypalPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -419,7 +454,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)?; + + match response { + // if card supports 3DS check for liability + paypal::PaypalPreProcessingResponse::PaypalLiabilityResponse(liability_response) => { + // permutation for status to continue payment + match ( + liability_response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .as_ref(), + liability_response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .as_ref(), + liability_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, + liability_response + .payment_source + .card + .authentication_result + .liability_shift, + liability_response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .unwrap_or(paypal::EnrollementStatus::Null), + liability_response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .unwrap_or(paypal::AuthenticationStatus::Null), + )), + status_code: res.status_code, + }), + ..data.clone() + }), + } + } + // if card does not supports 3DS check for liability + paypal::PaypalPreProcessingResponse::PaypalNonLiablityResponse(_) => { + 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() + }) + } + } + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration< CompleteAuthorize, @@ -522,9 +734,6 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, connectors, - )?) .build(), )) } @@ -696,7 +905,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -704,12 +913,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(paypal_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -724,7 +928,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = paypal::PaypalRefundRequest::try_from(&connector_router_data)?; - let paypal_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(paypal_req)) + let connector_req = paypal::PaypalRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -887,7 +1086,7 @@ impl ConnectorIntegration, _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![ ( @@ -1051,7 +1244,7 @@ impl .headers(types::VerifyWebhookSourceType::get_headers( self, req, connectors, )?) - .body(types::VerifyWebhookSourceType::get_request_body( + .set_body(types::VerifyWebhookSourceType::get_request_body( self, req, connectors, )?) .build(); @@ -1067,14 +1260,9 @@ impl types::VerifyWebhookSourceResponseData, >, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = paypal::PaypalSourceVerificationRequest::try_from(&req.request)?; - let paypal_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(paypal_req)) + ) -> CustomResult { + let connector_req = paypal::PaypalSourceVerificationRequest::try_from(&req.request)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -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 5468c6bb8061..88595585fe1b 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, }; @@ -173,13 +290,24 @@ fn get_payment_source( bank_name: _, country, } => Ok(PaymentSourceItem::Eps(RedirectRequest { - name: billing_details.get_billing_name()?, + name: billing_details + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "eps.billing_details", + })? + .get_billing_name()?, country_code: country.ok_or(errors::ConnectorError::MissingRequiredField { field_name: "eps.country", })?, 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 +322,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 { @@ -201,13 +335,24 @@ fn get_payment_source( bank_name: _, country, } => Ok(PaymentSourceItem::IDeal(RedirectRequest { - name: billing_details.get_billing_name()?, + name: billing_details + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "ideal.billing_details", + })? + .get_billing_name()?, country_code: country.ok_or(errors::ConnectorError::MissingRequiredField { field_name: "ideal.country", })?, 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 { @@ -215,11 +360,24 @@ fn get_payment_source( preferred_language: _, billing_details, } => Ok(PaymentSourceItem::Sofort(RedirectRequest { - name: billing_details.get_billing_name()?, - country_code: *country, + name: billing_details + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "sofort.billing_details", + })? + .get_billing_name()?, + country_code: country.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "sofort.country", + })?, 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 { .. } @@ -238,20 +396,31 @@ fn get_payment_source( | BankRedirectData::Trustly { .. } | BankRedirectData::OnlineBankingFpx { .. } | BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } - .into()) + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ))? } } } +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 +428,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("-")); @@ -287,7 +458,10 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP let payment_source = Some(PaymentSourceItem::Card(CardRequest { billing_address: get_address_info(item.router_data.address.billing.as_ref())?, expiry, - name: ccard.card_holder_name.clone(), + name: ccard + .card_holder_name + .clone() + .unwrap_or(Secret::new("".to_string())), number: Some(ccard.card_number.clone()), security_code: Some(ccard.card_cvc.clone()), attributes, @@ -306,25 +480,34 @@ 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: if item.router_data.address.shipping.is_some() + { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })); @@ -359,10 +542,9 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP | api_models::payments::WalletData::WeChatPayQr(_) | api_models::payments::WalletData::CashappQr(_) | api_models::payments::WalletData::SwishQr(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ))? } }, api::PaymentMethodData::BankRedirect(ref bank_redirection_data) => { @@ -374,18 +556,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,11 +606,11 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Crypto(_) - | api_models::payments::PaymentMethodData::Upi(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -441,10 +625,9 @@ impl TryFrom<&api_models::payments::CardRedirectData> for PaypalPaymentsRequest | api_models::payments::CardRedirectData::Benefit {} | api_models::payments::CardRedirectData::MomoAtm {} | api_models::payments::CardRedirectData::CardRedirect {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -463,10 +646,9 @@ impl TryFrom<&api_models::payments::PayLaterData> for PaypalPaymentsRequest { | api_models::payments::PayLaterData::WalleyRedirect {} | api_models::payments::PayLaterData::AlmaRedirect {} | api_models::payments::PayLaterData::AtomeRedirect {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -481,10 +663,9 @@ impl TryFrom<&api_models::payments::BankDebitData> for PaypalPaymentsRequest { | api_models::payments::BankDebitData::SepaBankDebit { .. } | api_models::payments::BankDebitData::BecsBankDebit { .. } | api_models::payments::BankDebitData::BacsBankDebit { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -508,10 +689,9 @@ impl TryFrom<&api_models::payments::BankTransferData> for PaypalPaymentsRequest | api_models::payments::BankTransferData::MandiriVaBankTransfer { .. } | api_models::payments::BankTransferData::Pix {} | api_models::payments::BankTransferData::Pse {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -536,10 +716,9 @@ impl TryFrom<&api_models::payments::VoucherData> for PaypalPaymentsRequest { | api_models::payments::VoucherData::FamilyMart(_) | api_models::payments::VoucherData::Seicomart(_) | api_models::payments::VoucherData::PayEasy(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -552,10 +731,9 @@ impl TryFrom<&api_models::payments::GiftCardData> for PaypalPaymentsRequest { match value { api_models::payments::GiftCardData::Givex(_) | api_models::payments::GiftCardData::PaySafeCard {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -604,19 +782,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)?, } } @@ -683,6 +940,86 @@ pub struct PaypalThreeDsResponse { links: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PaypalPreProcessingResponse { + PaypalLiabilityResponse(PaypalLiabilityResponse), + PaypalNonLiablityResponse(PaypalNonLiablityResponse), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalLiabilityResponse { + pub payment_source: CardParams, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalNonLiablityResponse { + payment_source: CardsData, +} + +#[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, @@ -864,6 +1201,7 @@ impl .invoice_id .clone() .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), ..item.data }) @@ -968,6 +1306,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 }) @@ -1004,6 +1343,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1053,6 +1393,7 @@ impl connector_metadata: Some(connector_meta), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1120,6 +1461,7 @@ impl .invoice_id .clone() .or(Some(item.response.supplementary_data.related_ids.order_id)), + incremental_authorization_allowed: None, }), ..item.data }) @@ -1221,6 +1563,7 @@ impl TryFrom> .response .invoice_id .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), amount_captured: Some(amount_captured), ..item.data @@ -1271,6 +1614,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..c44b4d5d6ffa 100644 --- a/crates/router/src/connector/payu.rs +++ b/crates/router/src/connector/payu.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; @@ -22,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -97,6 +98,7 @@ impl ConnectorCommon for Payu { message: response.status.status_desc, reason: response.status.code_literal, attempt_status: None, + connector_transaction_id: None, }) } } @@ -126,6 +128,20 @@ impl types::PaymentsResponseData, > for Payu { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Payu".to_string()) + .into(), + ) + } } impl api::PaymentToken for Payu {} @@ -245,15 +261,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = payu::PayuAuthUpdateRequest::try_from(req)?; - let payu_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = payu::PayuAuthUpdateRequest::try_from(req)?; - Ok(Some(payu_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -267,7 +278,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = payu::PayuPaymentsCaptureRequest::try_from(req)?; - let payu_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payu_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -445,7 +452,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = payu::PayuPaymentsRequest::try_from(req)?; - let payu_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payu_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -548,7 +550,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = payu::PayuRefundRequest::try_from(req)?; - let payu_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payu_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -638,7 +635,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/placetopay.rs b/crates/router/src/connector/placetopay.rs new file mode 100644 index 000000000000..ef51f1d6e4ba --- /dev/null +++ b/crates/router/src/connector/placetopay.rs @@ -0,0 +1,639 @@ +pub mod transformers; + +use std::fmt::Debug; + +use common_utils::request::RequestContent; +use error_stack::{IntoReport, ResultExt}; +use transformers as placetopay; + +use crate::{ + configs::settings, + connector::utils::{self}, + core::errors::{self, CustomResult}, + headers, + services::{ + self, + request::{self}, + ConnectorIntegration, ConnectorValidation, + }, + types::{ + self, + api::{self, enums, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::BytesExt, +}; + +#[derive(Debug, Clone)] +pub struct Placetopay; + +impl api::Payment for Placetopay {} +impl api::PaymentSession for Placetopay {} +impl api::ConnectorAccessToken for Placetopay {} +impl api::MandateSetup for Placetopay {} +impl api::PaymentAuthorize for Placetopay {} +impl api::PaymentSync for Placetopay {} +impl api::PaymentCapture for Placetopay {} +impl api::PaymentVoid for Placetopay {} +impl api::Refund for Placetopay {} +impl api::RefundExecute for Placetopay {} +impl api::RefundSync for Placetopay {} +impl api::PaymentToken for Placetopay {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Placetopay +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Placetopay +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Placetopay { + fn id(&self) -> &'static str { + "placetopay" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.placetopay.base_url.as_ref() + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayErrorResponse = res + .response + .parse_struct("PlacetopayErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.status.reason.to_owned(), + message: response.status.message.to_owned(), + reason: Some(response.status.message), + attempt_status: None, + connector_transaction_id: None, + }) + } +} + +impl ConnectorValidation for Placetopay { + fn validate_capture_method( + &self, + capture_method: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + enums::CaptureMethod::Manual => Ok(()), + enums::CaptureMethod::Automatic + | enums::CaptureMethod::ManualMultiple + | enums::CaptureMethod::Scheduled => Err(utils::construct_not_supported_error_report( + capture_method, + self.id(), + )), + } + } +} + +impl ConnectorIntegration + for Placetopay +{ +} + +impl ConnectorIntegration + for Placetopay +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Placetopay +{ + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Placetopay".to_string()) + .into(), + ) + } +} + +impl ConnectorIntegration + for Placetopay +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}/process", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = placetopay::PlacetopayRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let req_obj = placetopay::PlacetopayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayPaymentsResponse = res + .response + .parse_struct("Placetopay PlacetopayPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Placetopay +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}/query", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = placetopay::PlacetopayPsyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .set_body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayPaymentsResponse = res + .response + .parse_struct("placetopay PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Placetopay +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}/transaction", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = placetopay::PlacetopayNextActionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayPaymentsResponse = res + .response + .parse_struct("Placetopay PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Placetopay +{ + 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!("{}/transaction", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = placetopay::PlacetopayNextActionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + 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)?) + .set_body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayPaymentsResponse = res + .response + .parse_struct("Placetopay PaymentCancelResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Placetopay +{ + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}/transaction", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = placetopay::PlacetopayRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .set_body(types::RefundExecuteType::get_request_body( + self, req, connectors, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: placetopay::PlacetopayRefundResponse = res + .response + .parse_struct("placetopay PlacetopayRefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Placetopay +{ + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}/query", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = placetopay::PlacetopayRsyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .set_body(types::RefundSyncType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayRefundResponse = res + .response + .parse_struct("placetopay PlacetopayRefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Placetopay { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} diff --git a/crates/router/src/connector/placetopay/transformers.rs b/crates/router/src/connector/placetopay/transformers.rs new file mode 100644 index 000000000000..dfdd3f904df4 --- /dev/null +++ b/crates/router/src/connector/placetopay/transformers.rs @@ -0,0 +1,493 @@ +use api_models::payments; +use common_utils::date_time; +use diesel_models::enums; +use error_stack::{IntoReport, ResultExt}; +use masking::{PeekInterface, Secret}; +use ring::digest; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::{ + self, BrowserInformationData, CardData, PaymentsAuthorizeRequestData, + PaymentsSyncRequestData, RouterData, + }, + consts, + core::errors, + types::{self, api, storage::enums as storage_enums}, +}; + +pub struct PlacetopayRouterData { + pub amount: i64, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for PlacetopayRouterData +{ + type Error = error_stack::Report; + fn try_from( + (_currency_unit, _currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + Ok(Self { + amount, + router_data: item, + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayPaymentsRequest { + auth: PlacetopayAuth, + payment: PlacetopayPayment, + instrument: PlacetopayInstrument, + ip_address: Secret, + user_agent: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum PlacetopayAuthorizeAction { + Checkin, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayAuthType { + login: Secret, + tran_key: Secret, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayAuth { + login: Secret, + tran_key: Secret, + nonce: String, + seed: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayPayment { + reference: String, + description: String, + amount: PlacetopayAmount, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayAmount { + currency: storage_enums::Currency, + total: i64, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayInstrument { + card: PlacetopayCard, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayCard { + number: cards::CardNumber, + expiration: Secret, + cvv: Secret, +} + +impl TryFrom<&PlacetopayRouterData<&types::PaymentsAuthorizeRouterData>> + for PlacetopayPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &PlacetopayRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + let browser_info = item.router_data.request.get_browser_info()?; + let ip_address = browser_info.get_ip_address()?; + let user_agent = browser_info.get_user_agent()?; + let auth = PlacetopayAuth::try_from(&item.router_data.connector_auth_type)?; + let payment = PlacetopayPayment { + reference: item.router_data.connector_request_reference_id.clone(), + description: item.router_data.get_description()?, + amount: PlacetopayAmount { + currency: item.router_data.request.currency, + total: item.amount, + }, + }; + match item.router_data.request.payment_method_data.clone() { + payments::PaymentMethodData::Card(req_card) => { + let card = PlacetopayCard { + number: req_card.card_number.clone(), + expiration: req_card + .clone() + .get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned())?, + cvv: req_card.card_cvc.clone(), + }; + Ok(Self { + ip_address, + user_agent, + auth, + payment, + instrument: PlacetopayInstrument { + card: card.to_owned(), + }, + }) + } + payments::PaymentMethodData::Wallet(_) + | 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("Placetopay"), + ) + .into()) + } + } + } +} + +impl TryFrom<&types::ConnectorAuthType> for PlacetopayAuth { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + let placetopay_auth = PlacetopayAuthType::try_from(auth_type)?; + let nonce_bytes = utils::generate_random_bytes(16); + let now = error_stack::IntoReport::into_report(date_time::date_as_yyyymmddthhmmssmmmz()) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let seed = format!("{}+00:00", now.split_at(now.len() - 5).0); + let mut context = digest::Context::new(&digest::SHA256); + context.update(&nonce_bytes); + context.update(seed.as_bytes()); + context.update(placetopay_auth.tran_key.peek().as_bytes()); + let encoded_digest = base64::Engine::encode(&consts::BASE64_ENGINE, context.finish()); + let nonce = base64::Engine::encode(&consts::BASE64_ENGINE, &nonce_bytes); + Ok(Self { + login: placetopay_auth.login, + tran_key: encoded_digest.into(), + nonce, + seed, + }) + } +} + +impl TryFrom<&types::ConnectorAuthType> for PlacetopayAuthType { + type Error = error_stack::Report; + + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + if let types::ConnectorAuthType::BodyKey { api_key, key1 } = auth_type { + Ok(Self { + login: api_key.to_owned(), + tran_key: key1.to_owned(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType)? + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PlacetopayStatus { + Ok, + Failed, + Approved, + Rejected, + Pending, + PendingValidation, + PendingProcess, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayStatusResponse { + status: PlacetopayStatus, +} + +impl From for enums::AttemptStatus { + fn from(item: PlacetopayStatus) -> Self { + match item { + PlacetopayStatus::Approved | PlacetopayStatus::Ok => Self::Authorized, + PlacetopayStatus::Failed | PlacetopayStatus::Rejected => Self::Failure, + PlacetopayStatus::Pending + | PlacetopayStatus::PendingValidation + | PlacetopayStatus::PendingProcess => Self::Authorizing, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayPaymentsResponse { + status: PlacetopayStatusResponse, + internal_reference: u64, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PlacetopayPaymentsResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.status.status), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.internal_reference.to_string(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } +} + +// REFUND : +// Type definition for RefundRequest +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayRefundRequest { + auth: PlacetopayAuth, + internal_reference: u64, + action: PlacetopayNextAction, +} + +impl TryFrom<&types::RefundsRouterData> for PlacetopayRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + let internal_reference = item + .request + .connector_transaction_id + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let action = PlacetopayNextAction::Refund; + + Ok(Self { + auth, + internal_reference, + action, + }) + } +} + +impl From for enums::RefundStatus { + fn from(item: PlacetopayRefundStatus) -> Self { + match item { + PlacetopayRefundStatus::Refunded => Self::Success, + PlacetopayRefundStatus::Failed | PlacetopayRefundStatus::Rejected => Self::Failure, + PlacetopayRefundStatus::Pending | PlacetopayRefundStatus::PendingProcess => { + Self::Pending + } + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayRefundResponse { + status: PlacetopayRefundStatus, + internal_reference: u64, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PlacetopayRefundStatus { + Refunded, + Rejected, + Failed, + Pending, + PendingProcess, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.internal_reference.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayRsyncRequest { + auth: PlacetopayAuth, + internal_reference: u64, +} + +impl TryFrom<&types::RefundsRouterData> for PlacetopayRsyncRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + let internal_reference = item + .request + .connector_transaction_id + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self { + auth, + internal_reference, + }) + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.internal_reference.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayErrorResponse { + pub status: PlacetopayError, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayError { + pub status: PlacetopayErrorStatus, + pub message: String, + pub reason: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PlacetopayErrorStatus { + Failed, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayPsyncRequest { + auth: PlacetopayAuth, + internal_reference: u64, +} + +impl TryFrom<&types::PaymentsSyncRouterData> for PlacetopayPsyncRequest { + type Error = error_stack::Report; + + fn try_from(item: &types::PaymentsSyncRouterData) -> Result { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + let internal_reference = item + .request + .get_connector_transaction_id()? + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self { + auth, + internal_reference, + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayNextActionRequest { + auth: PlacetopayAuth, + internal_reference: u64, + action: PlacetopayNextAction, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum PlacetopayNextAction { + Refund, + Void, + Process, + Checkout, +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for PlacetopayNextActionRequest { + type Error = error_stack::Report; + + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + let internal_reference = item + .request + .connector_transaction_id + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let action = PlacetopayNextAction::Checkout; + Ok(Self { + auth, + internal_reference, + action, + }) + } +} + +impl TryFrom<&types::PaymentsCancelRouterData> for PlacetopayNextActionRequest { + type Error = error_stack::Report; + + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + let internal_reference = item + .request + .connector_transaction_id + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let action = PlacetopayNextAction::Void; + Ok(Self { + auth, + internal_reference, + action, + }) + } +} diff --git a/crates/router/src/connector/powertranz.rs b/crates/router/src/connector/powertranz.rs index 04851dd1781a..654128bb3fd2 100644 --- a/crates/router/src/connector/powertranz.rs +++ b/crates/router/src/connector/powertranz.rs @@ -3,7 +3,7 @@ pub mod transformers; use std::fmt::Debug; use api_models::enums::AuthenticationType; -use common_utils::ext_traits::ValueExt; +use common_utils::{ext_traits::ValueExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::ExposeInterface; @@ -27,7 +27,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -121,6 +121,7 @@ impl ConnectorCommon for Powertranz { message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -157,6 +158,20 @@ impl types::PaymentsResponseData, > for Powertranz { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Powertranz".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -195,14 +210,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = powertranz::PowertranzPaymentsRequest::try_from(req)?; - let powertranz_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(powertranz_req)) + ) -> CustomResult { + let connector_req = powertranz::PowertranzPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -220,7 +230,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let redirect_payload: powertranz::RedirectResponsePayload = req .request .get_redirect_response_payload()? .parse_value("PowerTranz RedirectResponsePayload") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; let spi_token = format!(r#""{}""#, redirect_payload.spi_token); - let powertranz_req = - types::RequestBody::log_and_get_request_body(&spi_token, |spi_token| { - Ok(spi_token.to_string()) - }) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(powertranz_req)) + Ok(RequestContent::Json(Box::new(spi_token))) } fn build_request( @@ -312,7 +317,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -376,14 +381,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = powertranz::PowertranzBaseRequest::try_from(&req.request)?; - let powertranz_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(powertranz_req)) + ) -> CustomResult { + let connector_req = powertranz::PowertranzBaseRequest::try_from(&req.request)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -399,7 +399,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = powertranz::PowertranzBaseRequest::try_from(&req.request)?; - let powertranz_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(powertranz_req)) + ) -> CustomResult { + let connector_req = powertranz::PowertranzBaseRequest::try_from(&req.request)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -490,7 +485,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = powertranz::PowertranzBaseRequest::try_from(req)?; - let powertranz_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(powertranz_req)) + ) -> CustomResult { + let connector_req = powertranz::PowertranzBaseRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -554,7 +544,7 @@ impl ConnectorIntegration, - ) -> 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..8dc3688da9eb 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -101,7 +101,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PowertranzPaymentsRequest type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { let source = match item.request.payment_method_data.clone() { - api::PaymentMethodData::Card(card) => Ok(Source::from(&card)), + api::PaymentMethodData::Card(card) => Source::try_from(&card), api::PaymentMethodData::Wallet(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::PayLater(_) @@ -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()?, @@ -210,15 +211,19 @@ impl TryFrom<&types::BrowserInformation> for BrowserInfo { }) }*/ -impl From<&Card> for Source { - fn from(card: &Card) -> Self { +impl TryFrom<&Card> for Source { + type Error = error_stack::Report; + fn try_from(card: &Card) -> Result { let card = PowertranzCard { - cardholder_name: card.card_holder_name.clone(), + cardholder_name: card + .card_holder_name + .clone() + .unwrap_or(Secret::new("".to_string())), card_pan: card.card_number.clone(), - card_expiration: card.get_expiry_date_as_yymm(), + card_expiration: card.get_expiry_date_as_yymm()?, card_cvv: card.card_cvc.clone(), }; - Self::Card(card) + Ok(Self::Card(card)) } } @@ -327,6 +332,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 +450,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 +461,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 6765fad2653d..fc14f37a2b2d 100644 --- a/crates/router/src/connector/prophetpay.rs +++ b/crates/router/src/connector/prophetpay.rs @@ -3,6 +3,7 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; +use common_utils::request::RequestContent; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as prophetpay; @@ -22,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -107,17 +108,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.status.to_string(), - message: response.title, - reason: Some(response.errors.to_string()), + 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, }) } } @@ -144,6 +145,20 @@ impl types::PaymentsResponseData, > for Prophetpay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Prophetpay".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -176,21 +191,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = prophetpay::ProphetpayTokenRequest::try_from(&connector_router_data)?; + let connector_req = prophetpay::ProphetpayTokenRequest::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)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -208,7 +218,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { 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 connector_req = + 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)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -309,7 +315,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -324,7 +330,7 @@ impl where types::PaymentsResponseData: Clone, { - let response: prophetpay::ProphetpayResponse = res + let response: prophetpay::ProphetpayCompleteAuthResponse = res .response .parse_struct("prophetpay ProphetpayResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -373,15 +379,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = prophetpay::ProphetpaySyncRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = 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)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -395,7 +396,7 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayResponse = res + let response: prophetpay::ProphetpaySyncResponse = res .response - .parse_struct("prophetpay ProphetpayResponse") + .parse_struct("prophetpay PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -431,9 +432,12 @@ impl ConnectorIntegration for Prophetpay { + /* fn get_headers( &self, req: &types::PaymentsCancelRouterData, @@ -461,43 +465,30 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = prophetpay::ProphetpayVoidRequest::try_from(req)?; + ) -> CustomResult { + let connector_req =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)) + Ok(RequestContent::Json(Box::new(connector_req))) } + */ fn build_request( &self, - req: &types::PaymentsCancelRouterData, - connectors: &settings::Connectors, + _req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Get) - .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(), - )) + Err(errors::ConnectorError::NotImplemented("Void flow not implemented".to_string()).into()) } + /* fn handle_response( &self, data: &types::PaymentsCancelRouterData, res: Response, ) -> CustomResult { - let response: prophetpay::ProphetpayResponse = res + let response: prophetpay::ProphetpayVoidResponse = res .response - .parse_struct("prophetpay ProphetpayResponse") + .parse_struct("prophetpay PaymentsCancelResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -512,6 +503,7 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + */ } impl ConnectorIntegration @@ -544,20 +536,15 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = prophetpay::ProphetpayRefundRequest::try_from(&connector_router_data)?; - let prophetpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(prophetpay_req)) + let connector_req = prophetpay::ProphetpayRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -572,7 +559,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = prophetpay::ProphetpayRefundSyncRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = 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)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -652,11 +634,11 @@ 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)?) - .body(types::RefundSyncType::get_request_body( + .set_body(types::RefundSyncType::get_request_body( self, req, connectors, )?) .build(), @@ -668,7 +650,7 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayRefundResponse = res + let response: prophetpay::ProphetpayRefundSyncResponse = res .response .parse_struct("prophetpay ProphetpayRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -706,7 +688,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 74071d5b85cb..23cffa3da229 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -7,7 +7,8 @@ use serde::{Deserialize, Serialize}; use url::Url; use crate::{ - connector::utils, + connector::utils::{self, to_connector_meta}, + consts as const_val, core::errors, services, types::{self, api, storage::enums}, @@ -159,7 +160,11 @@ impl TryFrom<&ProphetpayRouterData<&types::PaymentsAuthorizeRouterData>> ), } } else { - Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()) + Err(errors::ConnectorError::CurrencyNotSupported { + message: item.router_data.request.currency.to_string(), + connector: "Prophetpay", + } + .into()) } } } @@ -214,6 +219,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -266,10 +272,7 @@ impl TryFrom<&ProphetpayRouterData<&types::PaymentsCompleteAuthorizeRouterData>> Ok(Self { amount: item.amount.to_owned(), ref_info: item.router_data.connector_request_reference_id.to_owned(), - inquiry_reference: format!( - "inquiry_{}", - item.router_data.connector_request_reference_id - ), + 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, @@ -290,10 +293,19 @@ fn get_card_token( 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.insert( + pair.first() + .ok_or(errors::ConnectorError::ResponseDeserializationFailed)? + .to_string(), + pair.get(1) + .ok_or(errors::ConnectorError::ResponseDeserializationFailed)? + .to_string(), + ); } - queries + Ok(queries) }) + .transpose() + .into_report()? .ok_or(errors::ConnectorError::ResponseDeserializationFailed)?; for (key, val) in queries_params { @@ -304,8 +316,8 @@ fn get_card_token( Err(errors::ConnectorError::MissingRequiredField { field_name: "card_token", - }) - .into_report() + } + .into()) } #[derive(Debug, Clone, Serialize)] @@ -346,8 +358,8 @@ impl TryFrom<&types::PaymentsSyncRouterData> for ProphetpaySyncRequest { .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; Ok(Self { transaction_id, - ref_info: item.attempt_id.to_owned(), - inquiry_reference: format!("inquiry_{}", item.attempt_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), }) @@ -355,66 +367,174 @@ impl TryFrom<&types::PaymentsSyncRouterData> for ProphetpaySyncRequest { } #[derive(Debug, Clone, Deserialize)] -pub enum ProphetpayPaymentStatus { - Success, - #[serde(rename = "Transaction Approved")] - Charged, - Failure, - #[serde(rename = "Transaction Voided")] - Voided, - #[serde(rename = "Requires a card on file.")] - CardTokenNotFound, - #[serde(rename = "RefInfo and InquiryReference are duplicated")] - DuplicateValue, - #[serde(rename = "Profile is missing")] - MissingProfile, - #[serde(rename = "RefInfo is empty.")] - EmptyRef, -} - -impl From for enums::AttemptStatus { - fn from(item: ProphetpayPaymentStatus) -> Self { - match item { - ProphetpayPaymentStatus::Success | ProphetpayPaymentStatus::Charged => Self::Charged, - ProphetpayPaymentStatus::Failure - | ProphetpayPaymentStatus::CardTokenNotFound - | ProphetpayPaymentStatus::DuplicateValue - | ProphetpayPaymentStatus::MissingProfile - | ProphetpayPaymentStatus::EmptyRef => Self::Failure, - ProphetpayPaymentStatus::Voided => Self::Voided, +#[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::ResponseRouterData< + F, + ProphetpayCompleteAuthResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + 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 + }) } } } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ProphetpayResponse { - pub response_text: ProphetpayPaymentStatus, +pub struct ProphetpaySyncResponse { + success: bool, + pub response_text: String, #[serde(rename = "transactionID")] pub transaction_id: String, } -impl TryFrom> +impl + TryFrom> for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::ResponseRouterData, + item: types::ResponseRouterData, ) -> Result { - Ok(Self { - status: enums::AttemptStatus::from(item.response.response_text), - 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, - }), - ..item.data - }) + 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 + }) + } } } @@ -435,8 +555,8 @@ impl TryFrom<&types::PaymentsCancelRouterData> for ProphetpayVoidRequest { let transaction_id = item.request.connector_transaction_id.to_owned(); Ok(Self { transaction_id, - ref_info: item.attempt_id.to_owned(), - inquiry_reference: format!("inquiry_{}", item.attempt_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), }) @@ -447,6 +567,7 @@ impl TryFrom<&types::PaymentsCancelRouterData> for ProphetpayVoidRequest { #[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, @@ -459,47 +580,23 @@ impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for Prophet fn try_from( item: &ProphetpayRouterData<&types::RefundsRouterData>, ) -> Result { - let auth_data = ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; - let transaction_id = item.router_data.request.connector_transaction_id.to_owned(); - Ok(Self { - transaction_id, - amount: item.amount.to_owned(), - profile: auth_data.profile_id, - ref_info: item.router_data.request.refund_id.to_owned(), - inquiry_reference: format!("inquiry_{}", item.router_data.request.refund_id), - action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), - }) - } -} - -#[allow(dead_code)] -#[derive(Debug, Deserialize, Clone)] -pub enum RefundStatus { - Success, - Failure, - #[serde(rename = "Transaction Voided")] - Voided, - #[serde(rename = "Requires a card on file.")] - CardTokenNotFound, - #[serde(rename = "RefInfo and InquiryReference are duplicated")] - DuplicateValue, - #[serde(rename = "Profile is missing")] - MissingProfile, - #[serde(rename = "RefInfo is empty.")] - EmptyRef, -} - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Success - // in retrieving refund, if it is successful, it is shown as voided - | RefundStatus::Voided => Self::Success, - RefundStatus::Failure - | RefundStatus::CardTokenNotFound - | RefundStatus::DuplicateValue - | RefundStatus::MissingProfile - | RefundStatus::EmptyRef => Self::Failure, + 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()) } } } @@ -507,7 +604,9 @@ impl From for enums::RefundStatus { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProphetpayRefundResponse { - pub response_text: RefundStatus, + pub success: bool, + pub response_text: String, + pub tran_seq_number: Option, } impl TryFrom> @@ -517,20 +616,80 @@ impl TryFrom, ) -> Result { - 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::from(item.response.response_text), - }), - ..item.data - }) + 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, @@ -541,36 +700,11 @@ impl TryFrom<&types::RefundSyncRouterData> for ProphetpayRefundSyncRequest { fn try_from(item: &types::RefundSyncRouterData) -> Result { let auth_data = ProphetpayAuthType::try_from(&item.connector_auth_type)?; Ok(Self { - ref_info: item.attempt_id.to_owned(), + 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), }) } } - -impl TryFrom> - for types::RefundsRouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::RefundsResponseRouterData, - ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.data.request.connector_transaction_id.clone(), - refund_status: enums::RefundStatus::from(item.response.response_text), - }), - ..item.data - }) - } -} - -// Error Response body is yet to be confirmed with the connector -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ProphetpayErrorResponse { - pub status: u16, - pub title: String, - pub trace_id: String, - pub errors: serde_json::Value, -} diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index cd8893d0d7b1..5272a8ab21e8 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; -use common_utils::{date_time, ext_traits::StringExt}; +use common_utils::{date_time, ext_traits::StringExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, Report, ResultExt}; use masking::{ExposeInterface, PeekInterface}; @@ -10,9 +10,9 @@ use rand::distributions::{Alphanumeric, DistString}; use ring::hmac; use transformers as rapyd; +use super::utils as connector_utils; use crate::{ configs::settings, - connector::{utils as connector_utils, utils as conn_utils}, consts, core::errors::{self, CustomResult}, headers, logger, @@ -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); @@ -184,20 +185,15 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = rapyd::RapydPaymentsRequest::try_from(&connector_router_data)?; - let rapyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(rapyd_req)) + let connector_req = rapyd::RapydPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -213,8 +209,7 @@ impl let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12); let auth: rapyd::RapydAuthType = rapyd::RapydAuthType::try_from(&req.connector_auth_type)?; - let body = types::PaymentsAuthorizeType::get_request_body(self, req, connectors)? - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + let body = types::PaymentsAuthorizeType::get_request_body(self, req, connectors)?; let req_body = types::RequestBody::get_inner_value(body).expose(); let signature = self.generate_signature(&auth, "post", "/v1/payments", &req_body, ×tamp, &salt)?; @@ -234,7 +229,7 @@ impl self, req, connectors, )?) .headers(headers) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(); @@ -277,6 +272,20 @@ impl types::PaymentsResponseData, > for Rapyd { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Rapyd".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Rapyd {} @@ -497,20 +506,15 @@ impl &self, req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let req_obj = rapyd::CaptureRequest::try_from(&connector_router_data)?; - let rapyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(rapyd_req)) + let connector_req = rapyd::CaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -526,8 +530,7 @@ impl "/v1/payments/{}/capture", req.request.connector_transaction_id ); - let body = types::PaymentsCaptureType::get_request_body(self, req, connectors)? - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + let body = types::PaymentsCaptureType::get_request_body(self, req, connectors)?; let req_body = types::RequestBody::get_inner_value(body).expose(); let signature = self.generate_signature(&auth, "post", &url_path, &req_body, ×tamp, &salt)?; @@ -545,7 +548,7 @@ impl self, req, connectors, )?) .headers(headers) - .body(types::PaymentsCaptureType::get_request_body( + .set_body(types::PaymentsCaptureType::get_request_body( self, req, connectors, )?) .build(); @@ -638,21 +641,16 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = rapyd::RapydRefundRequest::try_from(&connector_router_data)?; - let rapyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let connector_req = rapyd::RapydRefundRequest::try_from(&connector_router_data)?; - Ok(Some(rapyd_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -663,8 +661,7 @@ impl services::ConnectorIntegration, _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let base64_signature = conn_utils::get_header_key_value("signature", request.headers)?; + let base64_signature = connector_utils::get_header_key_value("signature", request.headers)?; let signature = consts::BASE64_ENGINE_URL_SAFE .decode(base64_signature.as_bytes()) .into_report() @@ -764,11 +761,11 @@ impl api::IncomingWebhook for Rapyd { merchant_id: &str, connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let host = conn_utils::get_header_key_value("host", request.headers)?; + let host = connector_utils::get_header_key_value("host", request.headers)?; let connector = self.id(); let url_path = format!("https://{host}/webhooks/{merchant_id}/{connector}"); - let salt = conn_utils::get_header_key_value("salt", request.headers)?; - let timestamp = conn_utils::get_header_key_value("timestamp", request.headers)?; + let salt = connector_utils::get_header_key_value("salt", request.headers)?; + let timestamp = connector_utils::get_header_key_value("timestamp", request.headers)?; let stringify_auth = String::from_utf8(connector_webhook_secrets.secret.to_vec()) .into_report() .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) @@ -900,7 +897,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 +920,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..9fd664748e38 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -131,7 +131,10 @@ impl TryFrom<&RapydRouterData<&types::PaymentsAuthorizeRouterData>> for RapydPay number: ccard.card_number.to_owned(), expiration_month: ccard.card_exp_month.to_owned(), expiration_year: ccard.card_exp_year.to_owned(), - name: ccard.card_holder_name.to_owned(), + name: ccard + .card_holder_name + .to_owned() + .unwrap_or(Secret::new("".to_string())), cvv: ccard.card_cvc.to_owned(), }), address: None, @@ -458,6 +461,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 +490,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ) } @@ -499,6 +504,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/riskified.rs b/crates/router/src/connector/riskified.rs new file mode 100644 index 000000000000..29d57ee28cd9 --- /dev/null +++ b/crates/router/src/connector/riskified.rs @@ -0,0 +1,550 @@ +pub mod transformers; +use std::fmt::Debug; + +#[cfg(feature = "frm")] +use common_utils::request::RequestContent; +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, PeekInterface}; +use ring::hmac; +use transformers as riskified; + +#[cfg(feature = "frm")] +use super::utils::FrmTransactionRouterDataRequest; +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{self, request, ConnectorIntegration, ConnectorValidation}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + }, +}; +#[cfg(feature = "frm")] +use crate::{ + types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, + utils::BytesExt, +}; + +#[derive(Debug, Clone)] +pub struct Riskified; + +impl Riskified { + pub fn generate_authorization_signature( + &self, + auth: &riskified::RiskifiedAuthType, + payload: &str, + ) -> CustomResult { + let key = hmac::Key::new( + hmac::HMAC_SHA256, + auth.secret_token.clone().expose().as_bytes(), + ); + + let signature_value = hmac::sign(&key, payload.as_bytes()); + + let digest = signature_value.as_ref(); + + Ok(hex::encode(digest)) + } +} + +impl ConnectorCommonExt for Riskified +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let auth: riskified::RiskifiedAuthType = + riskified::RiskifiedAuthType::try_from(&req.connector_auth_type)?; + + let riskified_req = self.get_request_body(req, connectors)?; + + let binding = types::RequestBody::get_inner_value(riskified_req); + let payload = binding.peek(); + + let digest = self + .generate_authorization_signature(&auth, payload) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let header = vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + ), + ( + "X-RISKIFIED-SHOP-DOMAIN".to_string(), + auth.domain_name.clone().into(), + ), + ( + "X-RISKIFIED-HMAC-SHA256".to_string(), + request::Mask::into_masked(digest), + ), + ( + "Accept".to_string(), + "application/vnd.riskified.com; version=2".into(), + ), + ]; + + Ok(header) + } +} + +impl ConnectorCommon for Riskified { + fn id(&self) -> &'static str { + "riskified" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.riskified.base_url.as_ref() + } + + #[cfg(feature = "frm")] + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: riskified::ErrorResponse = res + .response + .parse_struct("ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(ErrorResponse { + status_code: res.status_code, + attempt_status: None, + code: crate::consts::NO_ERROR_CODE.to_string(), + message: response.error.message.clone(), + reason: None, + connector_transaction_id: None, + }) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for Riskified +{ + fn get_headers( + &self, + req: &frm_types::FrmCheckoutRouterData, + 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: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "/decide")) + } + + fn get_request_body( + &self, + req: &frm_types::FrmCheckoutRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = riskified::RiskifiedPaymentsCheckoutRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmCheckoutType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(frm_types::FrmCheckoutType::get_headers( + self, req, connectors, + )?) + .set_body(frm_types::FrmCheckoutType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmCheckoutRouterData, + res: Response, + ) -> CustomResult { + let response: riskified::RiskifiedPaymentsResponse = res + .response + .parse_struct("RiskifiedPaymentsResponse Checkout") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::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 api::Payment for Riskified {} +impl api::PaymentAuthorize for Riskified {} +impl api::PaymentSync for Riskified {} +impl api::PaymentVoid for Riskified {} +impl api::PaymentCapture for Riskified {} +impl api::MandateSetup for Riskified {} +impl api::ConnectorAccessToken for Riskified {} +impl api::PaymentToken for Riskified {} +impl api::Refund for Riskified {} +impl api::RefundExecute for Riskified {} +impl api::RefundSync for Riskified {} +impl ConnectorValidation for Riskified {} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for Riskified +{ +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for Riskified +{ + fn get_headers( + &self, + req: &frm_types::FrmTransactionRouterData, + 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: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + match req.is_payment_successful() { + Some(false) => Ok(format!( + "{}{}", + self.base_url(connectors), + "/checkout_denied" + )), + Some(true) => Ok(format!("{}{}", self.base_url(connectors), "/decision")), + None => Err(errors::ConnectorError::FlowNotSupported { + flow: "Transaction".to_owned(), + connector: req.connector.to_string(), + })?, + } + } + + fn get_request_body( + &self, + req: &frm_types::FrmTransactionRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + match req.is_payment_successful() { + Some(false) => { + let req_obj = riskified::TransactionFailedRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + Some(true) => { + let req_obj = riskified::TransactionSuccessRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + None => Err(errors::ConnectorError::FlowNotSupported { + flow: "Transaction".to_owned(), + connector: req.connector.to_owned(), + })?, + } + } + + fn build_request( + &self, + req: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmTransactionType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmTransactionType::get_headers( + self, req, connectors, + )?) + .set_body(frm_types::FrmTransactionType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmTransactionRouterData, + res: Response, + ) -> CustomResult { + let response: riskified::RiskifiedTransactionResponse = res + .response + .parse_struct("RiskifiedPaymentsResponse Transaction") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + match response { + riskified::RiskifiedTransactionResponse::FailedResponse(response_data) => { + ::try_from(types::ResponseRouterData { + response: response_data, + data: data.clone(), + http_code: res.status_code, + }) + } + riskified::RiskifiedTransactionResponse::SuccessResponse(response_data) => { + ::try_from(types::ResponseRouterData { + response: response_data, + data: data.clone(), + http_code: res.status_code, + }) + } + } + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for Riskified +{ + fn get_headers( + &self, + req: &frm_types::FrmFulfillmentRouterData, + 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: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "/fulfill")) + } + + fn get_request_body( + &self, + req: &frm_types::FrmFulfillmentRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = riskified::RiskifiedFullfillmentRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmFulfillmentType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmFulfillmentType::get_headers( + self, req, connectors, + )?) + .set_body(frm_types::FrmFulfillmentType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmFulfillmentRouterData, + res: Response, + ) -> CustomResult { + let response: riskified::RiskifiedFulfilmentResponse = res + .response + .parse_struct("RiskifiedFulfilmentResponse fulfilment") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + frm_types::FrmFulfillmentRouterData::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) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for Riskified +{ +} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Riskified +{ + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Riskified".to_string()) + .into(), + ) + } +} + +impl api::PaymentSession for Riskified {} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +#[cfg(feature = "frm")] +impl api::FraudCheck for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckSale for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckCheckout for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckTransaction for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckFulfillment for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckRecordReturn for Riskified {} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Riskified { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} diff --git a/crates/router/src/connector/riskified/transformers.rs b/crates/router/src/connector/riskified/transformers.rs new file mode 100644 index 000000000000..4f155f341f6d --- /dev/null +++ b/crates/router/src/connector/riskified/transformers.rs @@ -0,0 +1,7 @@ +#[cfg(feature = "frm")] +pub mod api; +pub mod auth; + +#[cfg(feature = "frm")] +pub use self::api::*; +pub use self::auth::*; diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs new file mode 100644 index 000000000000..de8884f03909 --- /dev/null +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -0,0 +1,597 @@ +use api_models::payments::AdditionalPaymentData; +use common_utils::{ext_traits::ValueExt, pii::Email}; +use error_stack::{self, ResultExt}; +use masking::Secret; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{ + connector::utils::{ + AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckTransactionRequest, RouterData, + }, + core::{errors, fraud_check::types as core_types}, + types::{ + self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, + ResponseId, ResponseRouterData, + }, +}; + +type Error = error_stack::Report; + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedPaymentsCheckoutRequest { + order: CheckoutRequest, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct CheckoutRequest { + id: String, + note: Option, + email: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + currency: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + updated_at: PrimitiveDateTime, + gateway: Option, + browser_ip: Option, + total_price: i64, + total_discounts: i64, + cart_token: String, + referring_site: String, + line_items: Vec, + discount_codes: Vec, + shipping_lines: Vec, + payment_details: Option, + customer: RiskifiedCustomer, + billing_address: Option, + shipping_address: Option, + source: Source, + client_details: ClientDetails, + vendor_name: String, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct PaymentDetails { + credit_card_bin: Option>, + credit_card_number: Option>, + credit_card_company: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ShippingLines { + price: i64, + title: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct DiscountCodes { + amount: i64, + code: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ClientDetails { + user_agent: Option, + accept_language: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedCustomer { + email: Option, + first_name: Option>, + last_name: Option>, + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + verified_email: bool, + id: String, + account_type: CustomerAccountType, + orders_count: i32, + phone: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum CustomerAccountType { + Guest, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderAddress { + first_name: Option>, + last_name: Option>, + address1: Option>, + country_code: Option, + city: Option, + province: Option>, + phone: Option>, + zip: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct LineItem { + price: i64, + quantity: i32, + title: String, + product_type: Option, + requires_shipping: Option, + product_id: Option, + category: Option, + brand: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "snake_case")] +pub enum Source { + DesktopWeb, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedMetadata { + vendor_name: String, + shipping_lines: Vec, +} + +impl TryFrom<&frm_types::FrmCheckoutRouterData> for RiskifiedPaymentsCheckoutRequest { + type Error = Error; + fn try_from(payment_data: &frm_types::FrmCheckoutRouterData) -> Result { + let metadata: RiskifiedMetadata = payment_data + .frm_metadata + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "frm_metadata", + })? + .parse_value("Riskified Metadata") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let billing_address = payment_data.get_billing_address_with_phone_number()?; + let shipping_address = payment_data.get_shipping_address_with_phone_number()?; + let address = payment_data.get_billing_address()?; + + Ok(Self { + order: CheckoutRequest { + id: payment_data.attempt_id.clone(), + email: payment_data.request.email.clone(), + created_at: common_utils::date_time::now(), + updated_at: common_utils::date_time::now(), + gateway: payment_data.request.gateway.clone(), + total_price: payment_data.request.amount, + cart_token: payment_data.attempt_id.clone(), + line_items: payment_data + .request + .get_order_details()? + .iter() + .map(|order_detail| LineItem { + price: order_detail.amount, + quantity: i32::from(order_detail.quantity), + title: order_detail.product_name.clone(), + product_type: order_detail.product_type.clone(), + requires_shipping: order_detail.requires_shipping, + product_id: order_detail.product_id.clone(), + category: order_detail.category.clone(), + brand: order_detail.brand.clone(), + }) + .collect::>(), + source: Source::DesktopWeb, + billing_address: OrderAddress::try_from(billing_address).ok(), + shipping_address: OrderAddress::try_from(shipping_address).ok(), + total_discounts: 0, + currency: payment_data.request.currency, + referring_site: "hyperswitch.io".to_owned(), + discount_codes: Vec::new(), + shipping_lines: metadata.shipping_lines, + customer: RiskifiedCustomer { + email: payment_data.request.email.clone(), + + first_name: address.get_first_name().ok().cloned(), + last_name: address.get_last_name().ok().cloned(), + created_at: common_utils::date_time::now(), + verified_email: false, + id: payment_data.get_customer_id()?, + account_type: CustomerAccountType::Guest, + orders_count: 0, + phone: billing_address + .clone() + .phone + .and_then(|phone_data| phone_data.number), + }, + browser_ip: payment_data + .request + .browser_info + .as_ref() + .and_then(|browser_info| browser_info.ip_address), + client_details: ClientDetails { + user_agent: payment_data + .request + .browser_info + .as_ref() + .and_then(|browser_info| browser_info.user_agent.clone()), + accept_language: payment_data.request.browser_info.as_ref().and_then( + |browser_info: &types::BrowserInformation| browser_info.language.clone(), + ), + }, + note: payment_data.description.clone(), + vendor_name: metadata.vendor_name, + payment_details: match payment_data.request.payment_method_data.as_ref() { + Some(AdditionalPaymentData::Card(card_info)) => Some(PaymentDetails { + credit_card_bin: card_info.card_isin.clone().map(Secret::new), + credit_card_number: card_info + .last4 + .clone() + .map(|last_four| format!("XXXX-XXXX-XXXX-{}", last_four)) + .map(Secret::new), + credit_card_company: card_info.card_network.clone(), + }), + Some(_) | None => None, + }, + }, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedPaymentsResponse { + order: OrderResponse, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderResponse { + id: String, + status: PaymentStatus, + description: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedFulfilmentResponse { + order: OrderFulfilmentResponse, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderFulfilmentResponse { + id: String, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FulfilmentStatus { + Fulfilled, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum PaymentStatus { + Captured, + Created, + Submitted, + Approved, + Declined, + Processing, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = Error; + fn try_from( + item: ResponseRouterData< + F, + RiskifiedPaymentsResponse, + T, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order.id), + status: storage_enums::FraudCheckStatus::from(item.response.order.status), + connector_metadata: None, + score: None, + reason: item.response.order.description.map(serde_json::Value::from), + }), + ..item.data + }) + } +} + +impl From for storage_enums::FraudCheckStatus { + fn from(item: PaymentStatus) -> Self { + match item { + PaymentStatus::Approved => Self::Legit, + PaymentStatus::Declined => Self::Fraud, + _ => Self::Pending, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionFailedRequest { + checkout: FailedTransactionData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct FailedTransactionData { + id: String, + payment_details: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct DeclinedPaymentDetails { + authorization_error: AuthorizationError, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct AuthorizationError { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + error_code: Option, + message: Option, +} + +impl TryFrom<&frm_types::FrmTransactionRouterData> for TransactionFailedRequest { + type Error = Error; + fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + Ok(Self { + checkout: FailedTransactionData { + id: item.attempt_id.clone(), + payment_details: [DeclinedPaymentDetails { + authorization_error: AuthorizationError { + created_at: common_utils::date_time::now(), + error_code: item.request.error_code.clone(), + message: item.request.error_message.clone(), + }, + }] + .to_vec(), + }, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedFailedTransactionResponse { + checkout: OrderResponse, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(untagged)] +pub enum RiskifiedTransactionResponse { + FailedResponse(RiskifiedFailedTransactionResponse), + SuccessResponse(RiskifiedPaymentsResponse), +} + +impl + TryFrom< + ResponseRouterData< + F, + RiskifiedFailedTransactionResponse, + T, + frm_types::FraudCheckResponseData, + >, + > for types::RouterData +{ + type Error = Error; + fn try_from( + item: ResponseRouterData< + F, + RiskifiedFailedTransactionResponse, + T, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.checkout.id), + status: storage_enums::FraudCheckStatus::from(item.response.checkout.status), + connector_metadata: None, + score: None, + reason: item + .response + .checkout + .description + .map(serde_json::Value::from), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionSuccessRequest { + order: SuccessfulTransactionData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct SuccessfulTransactionData { + id: String, + decision: TransactionDecisionData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionDecisionData { + external_status: TransactionStatus, + reason: Option, + amount: i64, + currency: storage_enums::Currency, + #[serde(with = "common_utils::custom_serde::iso8601")] + decided_at: PrimitiveDateTime, + payment_details: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionPaymentDetails { + authorization_id: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum TransactionStatus { + Approved, +} + +impl TryFrom<&frm_types::FrmTransactionRouterData> for TransactionSuccessRequest { + type Error = Error; + fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + Ok(Self { + order: SuccessfulTransactionData { + id: item.attempt_id.clone(), + decision: TransactionDecisionData { + external_status: TransactionStatus::Approved, + reason: None, + amount: item.request.amount, + currency: item.request.get_currency()?, + decided_at: common_utils::date_time::now(), + payment_details: [TransactionPaymentDetails { + authorization_id: item.request.connector_transaction_id.clone(), + }] + .to_vec(), + }, + }, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedFullfillmentRequest { + order: OrderFullfillment, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FulfillmentRequestStatus { + Success, + Cancelled, + Error, + Failure, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderFullfillment { + id: String, + fulfillments: FulfilmentData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct FulfilmentData { + fulfillment_id: String, + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + status: Option, + tracking_company: String, + tracking_number: String, + tracking_url: Option, +} + +impl TryFrom<&frm_types::FrmFulfillmentRouterData> for RiskifiedFullfillmentRequest { + type Error = Error; + fn try_from(item: &frm_types::FrmFulfillmentRouterData) -> Result { + Ok(Self { + order: OrderFullfillment { + id: item.attempt_id.clone(), + fulfillments: FulfilmentData { + fulfillment_id: item.payment_id.clone(), + created_at: common_utils::date_time::now(), + status: item + .request + .fulfillment_req + .fulfillment_status + .clone() + .and_then(get_fulfillment_status), + tracking_company: item + .request + .fulfillment_req + .tracking_company + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "tracking_company", + })?, + tracking_number: item.request.fulfillment_req.tracking_number.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "tracking_number", + }, + )?, + tracking_url: item.request.fulfillment_req.tracking_url.clone(), + }, + }, + }) + } +} + +impl + TryFrom< + ResponseRouterData< + Fulfillment, + RiskifiedFulfilmentResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + > + for types::RouterData< + Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > +{ + type Error = Error; + fn try_from( + item: ResponseRouterData< + Fulfillment, + RiskifiedFulfilmentResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::FulfillmentResponse { + order_id: item.response.order.id, + shipment_ids: Vec::new(), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ErrorResponse { + pub error: ErrorData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ErrorData { + pub message: String, +} + +impl TryFrom<&api_models::payments::Address> for OrderAddress { + type Error = Error; + fn try_from(address_info: &api_models::payments::Address) -> Result { + let address = + address_info + .clone() + .address + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "address", + })?; + Ok(Self { + first_name: address.first_name.clone(), + last_name: address.last_name.clone(), + address1: address.line1.clone(), + country_code: address.country, + city: address.city.clone(), + province: address.state.clone(), + zip: address.zip.clone(), + phone: address_info + .phone + .clone() + .and_then(|phone_data| phone_data.number), + }) + } +} + +fn get_fulfillment_status( + status: core_types::FulfillmentStatus, +) -> Option { + match status { + core_types::FulfillmentStatus::COMPLETE => Some(FulfillmentRequestStatus::Success), + core_types::FulfillmentStatus::CANCELED => Some(FulfillmentRequestStatus::Cancelled), + core_types::FulfillmentStatus::PARTIAL | core_types::FulfillmentStatus::REPLACEMENT => None, + } +} diff --git a/crates/router/src/connector/riskified/transformers/auth.rs b/crates/router/src/connector/riskified/transformers/auth.rs new file mode 100644 index 000000000000..6968bb55a59c --- /dev/null +++ b/crates/router/src/connector/riskified/transformers/auth.rs @@ -0,0 +1,22 @@ +use error_stack; +use masking::{ExposeInterface, Secret}; + +use crate::{core::errors, types}; + +pub struct RiskifiedAuthType { + pub secret_token: Secret, + pub domain_name: String, +} + +impl TryFrom<&types::ConnectorAuthType> for RiskifiedAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + secret_token: api_key.to_owned(), + domain_name: key1.to_owned().expose(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index 98eb895db548..c38fac56efc0 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::ext_traits::ByteSliceExt; +use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use transformers as shift4; @@ -26,7 +26,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -100,6 +100,7 @@ impl ConnectorCommon for Shift4 { message: response.error.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -157,6 +158,20 @@ impl types::PaymentsResponseData, > for Shift4 { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Shift4".to_string()) + .into(), + ) + } } #[async_trait::async_trait] @@ -187,14 +202,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = shift4::Shift4PaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } async fn execute_pretasks( @@ -250,7 +260,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = shift4::Shift4PaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -497,10 +502,9 @@ impl services::RequestBuilder::new() .method(services::Method::Post) .url(&types::PaymentsInitType::get_url(self, req, connectors)?) - .content_type(request::ContentType::FormUrlEncoded) .attach_default_headers() .headers(types::PaymentsInitType::get_headers(self, req, connectors)?) - .body(types::PaymentsInitType::get_request_body( + .set_body(types::PaymentsInitType::get_request_body( self, req, connectors, )?) .build(), @@ -563,14 +567,9 @@ impl &self, req: &types::PaymentsCompleteAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = shift4::Shift4PaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -587,7 +586,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -644,14 +643,9 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = shift4::Shift4RefundRequest::try_from(req)?; - let shift4_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(shift4_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -666,7 +660,7 @@ impl ConnectorIntegration, - ) -> 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..949082f4253a 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()), } } } @@ -313,7 +296,10 @@ impl number: card.card_number.clone(), exp_month: card.card_exp_month.clone(), exp_year: card.card_exp_year.clone(), - cardholder_name: card.card_holder_name.clone(), + cardholder_name: card + .card_holder_name + .clone() + .unwrap_or(Secret::new("".to_string())), }; if item.is_three_ds() { Ok(Self::Cards3DSRequest(Box::new(Cards3DSRequest { @@ -397,10 +383,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 +403,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 +413,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 +678,7 @@ impl ), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -739,6 +720,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/signifyd.rs b/crates/router/src/connector/signifyd.rs new file mode 100644 index 000000000000..544dfd137db4 --- /dev/null +++ b/crates/router/src/connector/signifyd.rs @@ -0,0 +1,638 @@ +pub mod transformers; +use std::fmt::Debug; + +#[cfg(feature = "frm")] +use common_utils::request::RequestContent; +use error_stack::{IntoReport, ResultExt}; +use masking::PeekInterface; +use transformers as signifyd; + +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{self, request, ConnectorIntegration, ConnectorValidation}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + }, +}; +#[cfg(feature = "frm")] +use crate::{ + types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, + utils::BytesExt, +}; + +#[derive(Debug, Clone)] +pub struct Signifyd; + +impl ConnectorCommonExt for Signifyd +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Signifyd { + fn id(&self) -> &'static str { + "signifyd" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.signifyd.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = signifyd::SignifydAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let auth_api_key = format!("Basic {}", auth.api_key.peek()); + + Ok(vec![( + headers::AUTHORIZATION.to_string(), + request::Mask::into_masked(auth_api_key), + )]) + } + + #[cfg(feature = "frm")] + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydErrorResponse = res + .response + .parse_struct("SignifydErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(ErrorResponse { + status_code: res.status_code, + code: crate::consts::NO_ERROR_CODE.to_string(), + message: response.messages.join(" &"), + reason: Some(response.errors.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } +} + +impl api::Payment for Signifyd {} +impl api::PaymentAuthorize for Signifyd {} +impl api::PaymentSync for Signifyd {} +impl api::PaymentVoid for Signifyd {} +impl api::PaymentCapture for Signifyd {} +impl api::MandateSetup for Signifyd {} +impl api::ConnectorAccessToken for Signifyd {} +impl api::PaymentToken for Signifyd {} +impl api::Refund for Signifyd {} +impl api::RefundExecute for Signifyd {} +impl api::RefundSync for Signifyd {} +impl ConnectorValidation for Signifyd {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Signifyd +{ + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Signifyd".to_string()) + .into(), + ) + } +} + +impl api::PaymentSession for Signifyd {} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration for Signifyd {} + +#[cfg(feature = "frm")] +impl api::FraudCheck for Signifyd {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckSale for Signifyd {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckCheckout for Signifyd {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckTransaction for Signifyd {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckFulfillment for Signifyd {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckRecordReturn for Signifyd {} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmSaleRouterData, + 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: &frm_types::FrmSaleRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/sales" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmSaleRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = signifyd::SignifydPaymentsSaleRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &frm_types::FrmSaleRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmSaleType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(frm_types::FrmSaleType::get_headers(self, req, connectors)?) + .set_body(frm_types::FrmSaleType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmSaleRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsResponse = res + .response + .parse_struct("SignifydPaymentsResponse Sale") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::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) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmCheckoutRouterData, + 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: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/checkouts" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmCheckoutRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = signifyd::SignifydPaymentsCheckoutRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmCheckoutType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(frm_types::FrmCheckoutType::get_headers( + self, req, connectors, + )?) + .set_body(frm_types::FrmCheckoutType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmCheckoutRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsResponse = res + .response + .parse_struct("SignifydPaymentsResponse Checkout") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::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) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmTransactionRouterData, + 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: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/transactions" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmTransactionRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = signifyd::SignifydPaymentsTransactionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmTransactionType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmTransactionType::get_headers( + self, req, connectors, + )?) + .set_body(frm_types::FrmTransactionType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmTransactionRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsResponse = res + .response + .parse_struct("SignifydPaymentsResponse Transaction") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::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) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmFulfillmentRouterData, + 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: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/fulfillments" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmFulfillmentRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = signifyd::FrmFullfillmentSignifydRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj.clone()))) + } + + fn build_request( + &self, + req: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmFulfillmentType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmFulfillmentType::get_headers( + self, req, connectors, + )?) + .set_body(frm_types::FrmFulfillmentType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmFulfillmentRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::FrmFullfillmentSignifydApiResponse = res + .response + .parse_struct("FrmFullfillmentSignifydApiResponse Sale") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + frm_types::FrmFulfillmentRouterData::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) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmRecordReturnRouterData, + 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: &frm_types::FrmRecordReturnRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/returns/records" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmRecordReturnRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = signifyd::SignifydPaymentsRecordReturnRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &frm_types::FrmRecordReturnRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmRecordReturnType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmRecordReturnType::get_headers( + self, req, connectors, + )?) + .set_body(frm_types::FrmRecordReturnType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmRecordReturnRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsRecordReturnResponse = res + .response + .parse_struct("SignifydPaymentsResponse Transaction") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Signifyd { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} diff --git a/crates/router/src/connector/signifyd/transformers.rs b/crates/router/src/connector/signifyd/transformers.rs new file mode 100644 index 000000000000..4f155f341f6d --- /dev/null +++ b/crates/router/src/connector/signifyd/transformers.rs @@ -0,0 +1,7 @@ +#[cfg(feature = "frm")] +pub mod api; +pub mod auth; + +#[cfg(feature = "frm")] +pub use self::api::*; +pub use self::auth::*; diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs new file mode 100644 index 000000000000..66d6f0e48cd5 --- /dev/null +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -0,0 +1,594 @@ +use bigdecimal::ToPrimitive; +use common_utils::pii::Email; +use error_stack; +use masking::Secret; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; +use utoipa::ToSchema; + +use crate::{ + connector::utils::{ + AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckRecordReturnRequest, + FraudCheckSaleRequest, FraudCheckTransactionRequest, RouterData, + }, + core::{errors, fraud_check::types as core_types}, + types::{ + self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, + ResponseId, ResponseRouterData, + }, +}; + +#[allow(dead_code)] +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DecisionDelivery { + Sync, + AsyncOnly, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Purchase { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + order_channel: OrderChannel, + total_price: i64, + products: Vec, + shipments: Shipments, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OrderChannel { + Web, + Phone, + MobileApp, + Social, + Marketplace, + InStoreKiosk, + ScanAndGo, + SmartTv, + Mit, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Products { + item_name: String, + item_price: i64, + item_quantity: i32, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +pub struct Shipments { + destination: Destination, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Destination { + full_name: Secret, + organization: Option, + email: Option, + address: Address, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Address { + street_address: Secret, + unit: Option>, + postal_code: Secret, + city: String, + province_code: Secret, + country_code: common_enums::CountryAlpha2, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsSaleRequest { + order_id: String, + purchase: Purchase, + decision_delivery: DecisionDelivery, +} + +impl TryFrom<&frm_types::FrmSaleRouterData> for SignifydPaymentsSaleRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmSaleRouterData) -> Result { + let products = item + .request + .get_order_details()? + .iter() + .map(|order_detail| Products { + item_name: order_detail.product_name.clone(), + item_price: order_detail.amount, + item_quantity: i32::from(order_detail.quantity), + }) + .collect::>(); + let ship_address = item.get_shipping_address()?; + let street_addr = ship_address.get_line1()?; + let city_addr = ship_address.get_city()?; + let zip_code_addr = ship_address.get_zip()?; + let country_code_addr = ship_address.get_country()?; + let _first_name_addr = ship_address.get_first_name()?; + let _last_name_addr = ship_address.get_last_name()?; + let address: Address = Address { + street_address: street_addr.clone(), + unit: None, + postal_code: zip_code_addr.clone(), + city: city_addr.clone(), + province_code: zip_code_addr.clone(), + country_code: country_code_addr.to_owned(), + }; + let destination: Destination = Destination { + full_name: ship_address.get_full_name().unwrap_or_default(), + organization: None, + email: None, + address, + }; + + let created_at = common_utils::date_time::now(); + let order_channel = OrderChannel::Web; + let shipments = Shipments { destination }; + let purchase = Purchase { + created_at, + order_channel, + total_price: item.request.amount, + products, + shipments, + }; + Ok(Self { + order_id: item.attempt_id.clone(), + purchase, + decision_delivery: DecisionDelivery::Sync, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Decision { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + checkpoint_action: SignifydPaymentStatus, + checkpoint_action_reason: Option, + checkpoint_action_policy: Option, + score: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum SignifydPaymentStatus { + Accept, + Challenge, + Credit, + Hold, + Reject, +} + +impl From for storage_enums::FraudCheckStatus { + fn from(item: SignifydPaymentStatus) -> Self { + match item { + SignifydPaymentStatus::Accept => Self::Legit, + SignifydPaymentStatus::Reject => Self::Fraud, + SignifydPaymentStatus::Hold => Self::ManualReview, + SignifydPaymentStatus::Challenge | SignifydPaymentStatus::Credit => Self::Pending, + } + } +} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsResponse { + signifyd_id: i64, + order_id: String, + decision: Decision, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order_id), + status: storage_enums::FraudCheckStatus::from( + item.response.decision.checkpoint_action, + ), + connector_metadata: None, + score: item.response.decision.score.and_then(|data| data.to_i32()), + reason: item + .response + .decision + .checkpoint_action_reason + .map(serde_json::Value::from), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct SignifydErrorResponse { + pub messages: Vec, + pub errors: serde_json::Value, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Transactions { + transaction_id: String, + gateway_status_code: String, + payment_method: storage_enums::PaymentMethod, + amount: i64, + currency: storage_enums::Currency, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsTransactionRequest { + order_id: String, + checkout_id: String, + transactions: Transactions, +} + +impl From for GatewayStatusCode { + fn from(item: storage_enums::AttemptStatus) -> Self { + match item { + storage_enums::AttemptStatus::Pending => Self::Pending, + storage_enums::AttemptStatus::Failure => Self::Failure, + storage_enums::AttemptStatus::Charged => Self::Success, + _ => Self::Pending, + } + } +} + +impl TryFrom<&frm_types::FrmTransactionRouterData> for SignifydPaymentsTransactionRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + let currency = item.request.get_currency()?; + let transactions = Transactions { + amount: item.request.amount, + transaction_id: item.clone().payment_id, + gateway_status_code: GatewayStatusCode::from(item.status).to_string(), + payment_method: item.payment_method, + currency, + }; + Ok(Self { + order_id: item.attempt_id.clone(), + checkout_id: item.payment_id.clone(), + transactions, + }) + } +} + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, +)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +pub enum GatewayStatusCode { + Success, + Failure, + #[default] + Pending, + Error, + Cancelled, + Expired, + SoftDecline, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsCheckoutRequest { + checkout_id: String, + order_id: String, + purchase: Purchase, +} + +impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmCheckoutRouterData) -> Result { + let products = item + .request + .get_order_details()? + .iter() + .map(|order_detail| Products { + item_name: order_detail.product_name.clone(), + item_price: order_detail.amount, + item_quantity: i32::from(order_detail.quantity), + }) + .collect::>(); + let ship_address = item.get_shipping_address()?; + let street_addr = ship_address.get_line1()?; + let city_addr = ship_address.get_city()?; + let zip_code_addr = ship_address.get_zip()?; + let country_code_addr = ship_address.get_country()?; + let _first_name_addr = ship_address.get_first_name()?; + let _last_name_addr = ship_address.get_last_name()?; + let address: Address = Address { + street_address: street_addr.clone(), + unit: None, + postal_code: zip_code_addr.clone(), + city: city_addr.clone(), + province_code: zip_code_addr.clone(), + country_code: country_code_addr.to_owned(), + }; + let destination: Destination = Destination { + full_name: ship_address.get_full_name().unwrap_or_default(), + organization: None, + email: None, + address, + }; + let created_at = common_utils::date_time::now(); + let order_channel = OrderChannel::Web; + let shipments: Shipments = Shipments { destination }; + let purchase = Purchase { + created_at, + order_channel, + total_price: item.request.amount, + products, + shipments, + }; + Ok(Self { + checkout_id: item.payment_id.clone(), + order_id: item.attempt_id.clone(), + purchase, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFullfillmentSignifydRequest { + pub order_id: String, + pub fulfillment_status: Option, + pub fulfillments: Vec, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde(untagged)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub enum FulfillmentStatus { + PARTIAL, + COMPLETE, + REPLACEMENT, + CANCELED, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct Fulfillments { + pub shipment_id: String, + pub products: Option>, + pub destination: Destination, +} + +#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct Product { + pub item_name: String, + pub item_quantity: i64, + pub item_id: String, +} + +impl TryFrom<&frm_types::FrmFulfillmentRouterData> for FrmFullfillmentSignifydRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmFulfillmentRouterData) -> Result { + Ok(Self { + order_id: item.request.fulfillment_req.order_id.clone(), + fulfillment_status: item + .request + .fulfillment_req + .fulfillment_status + .clone() + .map(|fulfillment_status| FulfillmentStatus::from(&fulfillment_status)), + fulfillments: item + .request + .fulfillment_req + .fulfillments + .iter() + .map(|f| Fulfillments::from(f.clone())) + .collect(), + }) + } +} + +impl From<&core_types::FulfillmentStatus> for FulfillmentStatus { + fn from(status: &core_types::FulfillmentStatus) -> Self { + match status { + core_types::FulfillmentStatus::PARTIAL => Self::PARTIAL, + core_types::FulfillmentStatus::COMPLETE => Self::COMPLETE, + core_types::FulfillmentStatus::REPLACEMENT => Self::REPLACEMENT, + core_types::FulfillmentStatus::CANCELED => Self::CANCELED, + } + } +} + +impl From for Fulfillments { + fn from(fulfillment: core_types::Fulfillments) -> Self { + Self { + shipment_id: fulfillment.shipment_id, + products: fulfillment + .products + .map(|products| products.iter().map(|p| Product::from(p.clone())).collect()), + destination: Destination::from(fulfillment.destination), + } + } +} + +impl From for Product { + fn from(product: core_types::Product) -> Self { + Self { + item_name: product.item_name, + item_quantity: product.item_quantity, + item_id: product.item_id, + } + } +} + +impl From for Destination { + fn from(destination: core_types::Destination) -> Self { + Self { + full_name: destination.full_name, + organization: destination.organization, + email: destination.email, + address: Address::from(destination.address), + } + } +} + +impl From for Address { + fn from(address: core_types::Address) -> Self { + Self { + street_address: address.street_address, + unit: address.unit, + postal_code: address.postal_code, + city: address.city, + province_code: address.province_code, + country_code: address.country_code, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFullfillmentSignifydApiResponse { + pub order_id: String, + pub shipment_ids: Vec, +} + +impl + TryFrom< + ResponseRouterData< + Fulfillment, + FrmFullfillmentSignifydApiResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + > + for types::RouterData< + Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + Fulfillment, + FrmFullfillmentSignifydApiResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::FulfillmentResponse { + order_id: item.response.order_id, + shipment_ids: item.response.shipment_ids, + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydRefund { + method: RefundMethod, + amount: String, + currency: storage_enums::Currency, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsRecordReturnRequest { + order_id: String, + return_id: String, + refund_transaction_id: Option, + refund: SignifydRefund, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RefundMethod { + StoreCredit, + OriginalPaymentInstrument, + NewPaymentInstrument, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsRecordReturnResponse { + return_id: String, + order_id: String, +} + +impl + TryFrom< + ResponseRouterData< + F, + SignifydPaymentsRecordReturnResponse, + T, + frm_types::FraudCheckResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + SignifydPaymentsRecordReturnResponse, + T, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::RecordReturnResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order_id), + return_id: Some(item.response.return_id.to_string()), + connector_metadata: None, + }), + ..item.data + }) + } +} + +impl TryFrom<&frm_types::FrmRecordReturnRouterData> for SignifydPaymentsRecordReturnRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmRecordReturnRouterData) -> Result { + let currency = item.request.get_currency()?; + let refund = SignifydRefund { + method: item.request.refund_method.clone(), + amount: item.request.amount.to_string(), + currency, + }; + Ok(Self { + return_id: uuid::Uuid::new_v4().to_string(), + refund_transaction_id: item.request.refund_transaction_id.clone(), + refund, + order_id: item.attempt_id.clone(), + }) + } +} diff --git a/crates/router/src/connector/signifyd/transformers/auth.rs b/crates/router/src/connector/signifyd/transformers/auth.rs new file mode 100644 index 000000000000..cc5867aea366 --- /dev/null +++ b/crates/router/src/connector/signifyd/transformers/auth.rs @@ -0,0 +1,20 @@ +use error_stack; +use masking::Secret; + +use crate::{core::errors, types}; + +pub struct SignifydAuthType { + pub api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for SignifydAuthType { + 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()), + } + } +} diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index 1d4d7e95dfa3..e210a851dfc9 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; use api_models::enums; use base64::Engine; -use common_utils::ext_traits::ByteSliceExt; +use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as square; @@ -28,7 +28,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -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, }) } } @@ -161,6 +162,20 @@ impl types::PaymentsResponseData, > for Square { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Square".to_string()) + .into(), + ) + } } #[async_trait::async_trait] @@ -247,15 +262,10 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = square::SquareTokenRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = square::SquareTokenRequest::try_from(req)?; - let square_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(square_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -269,7 +279,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -417,15 +427,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = square::SquarePaymentsRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = square::SquarePaymentsRequest::try_from(req)?; - let square_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(square_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -443,7 +448,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = square::SquareRefundRequest::try_from(req)?; - let square_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(square_req)) + ) -> CustomResult { + let connector_req = square::SquareRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -732,7 +732,7 @@ impl ConnectorIntegration, - ) -> 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..75bb8e68184a 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::ext_traits::ByteSliceExt; +use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; @@ -25,7 +25,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, domain, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -110,6 +110,7 @@ impl ConnectorCommon for Stax { .to_owned(), ), attempt_status: None, + connector_transaction_id: None, }) } } @@ -162,15 +163,10 @@ impl &self, req: &types::ConnectorCustomerRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stax::StaxCustomerRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = stax::StaxCustomerRequest::try_from(req)?; - let stax_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stax_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -188,7 +184,7 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body( + .set_body(types::ConnectorCustomerType::get_request_body( self, req, connectors, )?) .build(), @@ -254,15 +250,10 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stax::StaxTokenRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = stax::StaxTokenRequest::try_from(req)?; - let stax_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stax_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -276,7 +267,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -329,6 +320,20 @@ impl types::PaymentsResponseData, > for Stax { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Stax".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -358,21 +363,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = stax::StaxRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = stax::StaxPaymentsRequest::try_from(&connector_router_data)?; + let connector_req = stax::StaxPaymentsRequest::try_from(&connector_router_data)?; - let stax_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stax_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -390,7 +390,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = stax::StaxRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -531,12 +531,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stax_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -552,7 +547,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = stax::StaxRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = stax::StaxRefundRequest::try_from(&connector_router_data)?; - let stax_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stax_req)) + let connector_req = stax::StaxRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -714,7 +704,7 @@ impl ConnectorIntegration, - ) -> 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..596ea1145ecc 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"), + ))?, } } } @@ -148,7 +147,7 @@ pub struct StaxCustomerRequest { #[serde(skip_serializing_if = "Option::is_none")] email: Option, #[serde(skip_serializing_if = "Option::is_none")] - firstname: Option, + firstname: Option>, } impl TryFrom<&types::ConnectorCustomerRouterData> for StaxCustomerRequest { @@ -226,8 +225,10 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { api::PaymentMethodData::Card(card_data) => { let stax_card_data = StaxTokenizeData { card_exp: card_data - .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), - person_name: card_data.card_holder_name, + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, + person_name: card_data + .card_holder_name + .unwrap_or(Secret::new("".to_string())), card_number: card_data.card_number, card_cvv: card_data.card_cvc, customer_id: Secret::new(customer_id), @@ -268,10 +269,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 +369,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..c151c5af455a 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::{collections::HashMap, fmt::Debug, ops::Deref}; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; @@ -26,7 +27,7 @@ use crate::{ self, api::{self, ConnectorCommon}, }, - utils::{self, crypto, ByteSliceExt, BytesExt, OptionExt}, + utils::{crypto, ByteSliceExt, BytesExt, OptionExt}, }; #[derive(Debug, Clone)] @@ -145,15 +146,10 @@ impl &self, req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req = stripe::StripeCreditTransferSourceRequest::try_from(req)?; - let pre_processing_request = types::RequestBody::log_and_get_request_body( - &req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = stripe::StripeCreditTransferSourceRequest::try_from(req)?; - Ok(Some(pre_processing_request)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -171,7 +167,7 @@ impl .headers(types::PaymentsPreProcessingType::get_headers( self, req, connectors, )?) - .body(types::PaymentsPreProcessingType::get_request_body( + .set_body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -227,6 +223,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -272,14 +269,9 @@ impl &self, req: &types::ConnectorCustomerRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stripe::CustomerRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::CustomerRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -297,7 +289,7 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body( + .set_body(types::ConnectorCustomerType::get_request_body( self, req, connectors, )?) .build(), @@ -357,6 +349,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -402,14 +395,9 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stripe::TokenRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::TokenRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -423,7 +411,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -483,6 +471,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -534,14 +523,9 @@ impl &self, req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stripe::CaptureRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::CaptureRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -557,7 +541,7 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body( + .set_body(types::PaymentsCaptureType::get_request_body( self, req, connectors, )?) .build(), @@ -617,6 +601,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -681,9 +666,6 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body( - self, req, connectors, - )?) .build(), )) } @@ -760,6 +742,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -823,19 +806,14 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { match &req.request.payment_method_data { api_models::payments::PaymentMethodData::BankTransfer(bank_transfer_data) => { stripe::get_bank_transfer_request_data(req, bank_transfer_data.deref()) } _ => { - let req = stripe::PaymentIntentRequest::try_from(req)?; - let request = types::RequestBody::log_and_get_request_body( - &req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(request)) + let connector_req = stripe::PaymentIntentRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } } } @@ -855,7 +833,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -918,6 +896,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -966,14 +945,9 @@ impl &self, req: &types::PaymentsCancelRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stripe::CancelRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::CancelRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -986,7 +960,7 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body( + .set_body(types::PaymentsVoidType::get_request_body( self, req, connectors, )?) .build(); @@ -1041,6 +1015,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1103,14 +1078,9 @@ impl types::PaymentsResponseData, >, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req = stripe::SetupIntentRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::SetupIntentRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -1128,7 +1098,7 @@ impl .url(&Verify::get_url(self, req, connectors)?) .attach_default_headers() .headers(Verify::get_headers(self, req, connectors)?) - .body(Verify::get_request_body(self, req, connectors)?) + .set_body(Verify::get_request_body(self, req, connectors)?) .build(), )) } @@ -1197,6 +1167,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1240,14 +1211,9 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stripe::RefundRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::RefundRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -1262,7 +1228,7 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { - let stripe_req = transformers::construct_file_upload_request(req.clone())?; - Ok(Some(stripe_req)) + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = transformers::construct_file_upload_request(req.clone())?; + Ok(RequestContent::FormData(connector_req)) } fn build_request( @@ -1511,8 +1477,9 @@ impl .url(&types::UploadFileType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::UploadFileType::get_headers(self, req, connectors)?) - .form_data(types::UploadFileType::get_request_form_data(self, req)?) - .content_type(services::request::ContentType::FormData) + .set_body(types::UploadFileType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -1569,6 +1536,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1672,6 +1640,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1722,14 +1691,9 @@ impl &self, req: &types::SubmitEvidenceRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let stripe_req = stripe::Evidence::try_from(req)?; - let stripe_req_string = types::RequestBody::log_and_get_request_body( - &stripe_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req_string)) + ) -> CustomResult { + let connector_req = stripe::Evidence::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -1744,7 +1708,7 @@ impl .headers(types::SubmitEvidenceType::get_headers( self, req, connectors, )?) - .body(types::SubmitEvidenceType::get_request_body( + .set_body(types::SubmitEvidenceType::get_request_body( self, req, connectors, )?) .build(); @@ -1801,6 +1765,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1893,10 +1858,15 @@ impl api::IncomingWebhook for Stripe { Ok(match details.event_data.event_object.object { stripe::WebhookEventObjectType::PaymentIntent => { - match details.event_data.event_object.metadata { + match details + .event_data + .event_object + .metadata + .and_then(|meta_data| meta_data.order_id) + { // if order_id is present - Some(meta_data) => api_models::webhooks::ObjectReferenceId::PaymentId( - api_models::payments::PaymentIdType::PaymentAttemptId(meta_data.order_id), + Some(order_id) => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId(order_id), ), // else used connector_transaction_id None => api_models::webhooks::ObjectReferenceId::PaymentId( @@ -1907,10 +1877,15 @@ impl api::IncomingWebhook for Stripe { } } stripe::WebhookEventObjectType::Charge => { - match details.event_data.event_object.metadata { + match details + .event_data + .event_object + .metadata + .and_then(|meta_data| meta_data.order_id) + { // if order_id is present - Some(meta_data) => api_models::webhooks::ObjectReferenceId::PaymentId( - api_models::payments::PaymentIdType::PaymentAttemptId(meta_data.order_id), + Some(order_id) => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId(order_id), ), // else used connector_transaction_id None => api_models::webhooks::ObjectReferenceId::PaymentId( @@ -1943,14 +1918,25 @@ impl api::IncomingWebhook for Stripe { ) } stripe::WebhookEventObjectType::Refund => { - match details.event_data.event_object.metadata { + match details + .event_data + .event_object + .metadata + .clone() + .and_then(|meta_data| meta_data.order_id) + { // if meta_data is present - Some(meta_data) => { + Some(order_id) => { // Issue: 2076 - match meta_data.is_refund_id_as_reference { + match details + .event_data + .event_object + .metadata + .and_then(|meta_data| meta_data.is_refund_id_as_reference) + { // if the order_id is refund_id Some(_) => api_models::webhooks::ObjectReferenceId::RefundId( - api_models::webhooks::RefundIdType::RefundId(meta_data.order_id), + api_models::webhooks::RefundIdType::RefundId(order_id), ), // if the order_id is payment_id // since payment_id was being passed before the deployment of this pr @@ -1991,6 +1977,9 @@ impl api::IncomingWebhook for Stripe { stripe::WebhookEventType::PaymentIntentCanceled => { api::IncomingWebhookEvent::PaymentIntentCancelled } + stripe::WebhookEventType::PaymentIntentAmountCapturableUpdated => { + api::IncomingWebhookEvent::PaymentIntentAuthorizationSuccess + } stripe::WebhookEventType::ChargeSucceeded => { if let Some(stripe::WebhookPaymentMethodDetails { payment_method: @@ -2047,7 +2036,6 @@ impl api::IncomingWebhook for Stripe { | stripe::WebhookEventType::ChargeRefunded | stripe::WebhookEventType::PaymentIntentCreated | stripe::WebhookEventType::PaymentIntentProcessing - | stripe::WebhookEventType::PaymentIntentAmountCapturableUpdated | stripe::WebhookEventType::SourceTransactionCreated => { api::IncomingWebhookEvent::EventNotSupported } @@ -2057,13 +2045,13 @@ impl api::IncomingWebhook for Stripe { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> 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 3f0d4f543ba4..8d397d0ebb9a 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1,22 +1,25 @@ -use std::ops::Deref; +use std::{collections::HashMap, ops::Deref}; use api_models::{self, enums as api_enums, payments}; use common_utils::{ errors::CustomResult, ext_traits::{ByteSliceExt, BytesExt}, pii::{self, Email}, + request::RequestContent, }; use data_models::mandates::AcceptanceType; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, ExposeOptionInterface, PeekInterface, Secret}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use time::PrimitiveDateTime; use url::Url; -use uuid::Uuid; use crate::{ collect_missing_value_keys, - connector::utils::{self as connector_util, ApplePay, PaymentsPreProcessingData, RouterData}, + connector::utils::{ + self as connector_util, ApplePay, ApplePayDecrypt, PaymentsPreProcessingData, RouterData, + }, core::errors, services, types::{ @@ -24,7 +27,7 @@ use crate::{ storage::enums, transformers::{ForeignFrom, ForeignTryFrom}, }, - utils::{self, OptionExt}, + utils::OptionExt, }; pub struct StripeAuthType { @@ -102,7 +105,7 @@ pub struct PaymentIntentRequest { pub statement_descriptor_suffix: Option, pub statement_descriptor: Option, #[serde(flatten)] - pub meta_data: StripeMetadata, + pub meta_data: HashMap, pub return_url: String, pub confirm: bool, pub mandate: Option>, @@ -132,7 +135,7 @@ pub struct PaymentIntentRequest { pub struct StripeMetadata { // merchant_reference_id #[serde(rename(serialize = "metadata[order_id]"))] - pub order_id: String, + pub order_id: Option, // to check whether the order_id is refund_id or payemnt_id // before deployment, order id is set to payemnt_id in refunds but now it is set as refund_id // it is set as string instead of bool because stripe pass it as string even if we set it as bool @@ -142,12 +145,6 @@ pub struct StripeMetadata { #[derive(Debug, Eq, PartialEq, Serialize)] pub struct SetupIntentRequest { - #[serde(rename = "metadata[order_id]")] - pub metadata_order_id: String, - #[serde(rename = "metadata[txn_id]")] - pub metadata_txn_id: String, - #[serde(rename = "metadata[txn_uuid]")] - pub metadata_txn_uuid: String, pub confirm: bool, pub usage: Option, pub customer: Option>, @@ -156,6 +153,8 @@ pub struct SetupIntentRequest { #[serde(flatten)] pub payment_data: StripePaymentMethodData, pub payment_method_options: Option, // For mandate txns using network_txns_id, needs to be validated + #[serde(flatten)] + pub meta_data: Option>, } #[derive(Debug, Eq, PartialEq, Serialize)] @@ -215,6 +214,8 @@ pub struct ChargesRequest { pub currency: String, pub customer: Secret, pub source: Secret, + #[serde(flatten)] + pub meta_data: Option>, } #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] @@ -287,7 +288,7 @@ pub struct StripeSofort { #[serde(rename = "payment_method_data[type]")] pub payment_method_data_type: StripePaymentMethodType, #[serde(rename = "payment_method_options[sofort][preferred_language]")] - preferred_language: String, + preferred_language: Option, #[serde(rename = "payment_method_data[sofort][country]")] country: api_enums::CountryAlpha2, } @@ -1079,16 +1080,30 @@ impl From<&payments::BankDebitBilling> for StripeBillingAddress { } } -impl TryFrom<&payments::BankRedirectData> for StripeBillingAddress { - type Error = errors::ConnectorError; +impl TryFrom<(&payments::BankRedirectData, Option)> for StripeBillingAddress { + type Error = error_stack::Report; - fn try_from(bank_redirection_data: &payments::BankRedirectData) -> Result { + fn try_from( + (bank_redirection_data, is_customer_initiated_mandate_payment): ( + &payments::BankRedirectData, + Option, + ), + ) -> Result { match bank_redirection_data { payments::BankRedirectData::Eps { billing_details, .. - } => Ok(Self { - name: billing_details.billing_name.clone(), - ..Self::default() + } => Ok({ + let billing_data = billing_details.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "billing_details", + }, + )?; + Self { + name: Some(connector_util::BankRedirectBillingData::get_billing_name( + &billing_data, + )?), + ..Self::default() + } }), payments::BankRedirectData::Giropay { billing_details, .. @@ -1098,11 +1113,10 @@ impl TryFrom<&payments::BankRedirectData> for StripeBillingAddress { }), payments::BankRedirectData::Ideal { billing_details, .. - } => Ok(Self { - name: billing_details.billing_name.clone(), - email: billing_details.email.clone(), - ..Self::default() - }), + } => Ok(get_stripe_sepa_dd_mandate_billing_details( + billing_details, + is_customer_initiated_mandate_payment, + )?), payments::BankRedirectData::Przelewy24 { billing_details, .. } => Ok(Self { @@ -1141,11 +1155,11 @@ impl TryFrom<&payments::BankRedirectData> for StripeBillingAddress { } payments::BankRedirectData::Sofort { billing_details, .. - } => Ok(Self { - name: billing_details.billing_name.clone(), - email: billing_details.email.clone(), - ..Self::default() - }), + } => Ok(get_stripe_sepa_dd_mandate_billing_details( + billing_details, + is_customer_initiated_mandate_payment, + )?), + payments::BankRedirectData::Bizum {} | payments::BankRedirectData::Blik { .. } | payments::BankRedirectData::Interac { .. } @@ -1227,6 +1241,7 @@ fn create_stripe_payment_method( payment_method_data: &api_models::payments::PaymentMethodData, auth_type: enums::AuthenticationType, payment_method_token: Option, + is_customer_initiated_mandate_payment: Option, ) -> Result< ( StripePaymentMethodData, @@ -1259,7 +1274,10 @@ fn create_stripe_payment_method( )) } payments::PaymentMethodData::BankRedirect(bank_redirect_data) => { - let billing_address = StripeBillingAddress::try_from(bank_redirect_data)?; + let billing_address = StripeBillingAddress::try_from(( + bank_redirect_data, + is_customer_initiated_mandate_payment, + ))?; let pm_type = StripePaymentMethodType::try_from(bank_redirect_data)?; let bank_redirect_data = StripePaymentMethodData::try_from(bank_redirect_data)?; @@ -1431,13 +1449,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()), } } @@ -1473,24 +1491,8 @@ impl TryFrom<(&payments::WalletData, Option)> if let Some(types::PaymentMethodToken::ApplePayDecrypt(decrypt_data)) = payment_method_token { - let expiry_year_4_digit = Secret::new(format!( - "20{}", - decrypt_data - .clone() - .application_expiration_date - .peek() - .get(0..2) - .ok_or(errors::ConnectorError::RequestEncodingFailed)? - )); - let exp_month = Secret::new( - decrypt_data - .clone() - .application_expiration_date - .peek() - .get(2..4) - .ok_or(errors::ConnectorError::RequestEncodingFailed)? - .to_owned(), - ); + let expiry_year_4_digit = decrypt_data.get_four_digit_expiry_year()?; + let exp_month = decrypt_data.get_expiry_month()?; Some(Self::Wallet(StripeWallet::ApplePayPredecryptToken( Box::new(StripeApplePayPredecrypt { @@ -1644,8 +1646,10 @@ impl TryFrom<&payments::BankRedirectData> for StripePaymentMethodData { } => Ok(Self::BankRedirect(StripeBankRedirectData::StripeSofort( Box::new(StripeSofort { payment_method_data_type, - country: country.to_owned(), - preferred_language: preferred_language.to_owned(), + country: country.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "sofort.country", + })?, + preferred_language: preferred_language.clone(), }), ))), payments::BankRedirectData::OnlineBankingFpx { .. } => { @@ -1764,6 +1768,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest { &item.request.payment_method_data, item.auth_type, item.payment_method_token.clone(), + Some(connector_util::PaymentsAuthorizeRequestData::is_customer_initiated_mandate_payment( + &item.request, + )), )?; validate_shipping_address_against_payment_method( @@ -1868,15 +1875,15 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest { None } }); + + let meta_data = get_transaction_metadata(item.request.metadata.clone(), order_id); + Ok(Self { amount: item.request.amount, //hopefully we don't loose some cents here currency: item.request.currency.to_string(), //we need to copy the value and not transfer ownership statement_descriptor_suffix: item.request.statement_descriptor_suffix.clone(), statement_descriptor: item.request.statement_descriptor.clone(), - meta_data: StripeMetadata { - order_id, - is_refund_id_as_reference: None, - }, + meta_data, return_url: item .request .router_return_url @@ -1936,10 +1943,6 @@ fn get_payment_method_type_for_saved_payment_method_payment( impl TryFrom<&types::SetupMandateRouterData> for SetupIntentRequest { type Error = error_stack::Report; fn try_from(item: &types::SetupMandateRouterData) -> Result { - let metadata_order_id = item.connector_request_reference_id.clone(); - let metadata_txn_id = format!("{}_{}_{}", item.merchant_id, item.payment_id, "1"); - let metadata_txn_uuid = Uuid::new_v4().to_string(); - //Only cards supported for mandates let pm_type = StripePaymentMethodType::Card; let payment_data = StripePaymentMethodData::try_from(( @@ -1948,17 +1951,20 @@ impl TryFrom<&types::SetupMandateRouterData> for SetupIntentRequest { pm_type, ))?; + let meta_data = Some(get_transaction_metadata( + item.request.metadata.clone(), + item.connector_request_reference_id.clone(), + )); + Ok(Self { confirm: true, - metadata_order_id, - metadata_txn_id, - metadata_txn_uuid, payment_data, return_url: item.request.router_return_url.clone(), off_session: item.request.off_session, usage: item.request.setup_future_usage, payment_method_options: None, customer: item.connector_customer.to_owned().map(Secret::new), + meta_data, }) } } @@ -1970,6 +1976,7 @@ impl TryFrom<&types::TokenizationRouterData> for TokenRequest { &item.request.payment_method_data, item.auth_type, item.payment_method_token.clone(), + None, )?; Ok(Self { token_data: payment_data.0, @@ -1984,7 +1991,7 @@ impl TryFrom<&types::ConnectorCustomerRouterData> for CustomerRequest { description: item.request.description.to_owned(), email: item.request.email.to_owned(), phone: item.request.phone.to_owned(), - name: item.request.name.to_owned().map(Secret::new), + name: item.request.name.to_owned(), source: item.request.preprocessing_id.to_owned(), }) } @@ -2334,6 +2341,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 @@ -2344,11 +2352,11 @@ impl pub fn get_connector_metadata( next_action: Option<&StripeNextActionResponse>, amount: i64, -) -> CustomResult, errors::ConnectorError> { +) -> CustomResult, errors::ConnectorError> { let next_action_response = next_action .and_then(|next_action_response| match next_action_response { StripeNextActionResponse::DisplayBankTransferInstructions(response) => { - let bank_instructions = response.financial_addresses.get(0); + let bank_instructions = response.financial_addresses.first(); let (sepa_bank_instructions, bacs_bank_instructions) = bank_instructions.map_or((None, None), |financial_address| { ( @@ -2480,6 +2488,7 @@ impl .or(Some(error.message.clone())), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }); let connector_metadata = @@ -2493,6 +2502,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: Some(item.response.id.clone()), + incremental_authorization_allowed: None, }), Err, ); @@ -2534,6 +2544,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 }) @@ -2705,7 +2716,7 @@ impl TryFrom<&types::RefundsRouterData> for RefundRequest { amount: Some(amount), payment_intent, meta_data: StripeMetadata { - order_id: item.request.refund_id.clone(), + order_id: Some(item.request.refund_id.clone()), is_refund_id_as_reference: Some("true".to_string()), }, }) @@ -2788,6 +2799,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)] @@ -2988,6 +3005,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"), ) @@ -3037,12 +3055,20 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for ChargesRequest { type Error = error_stack::Report; fn try_from(value: &types::PaymentsAuthorizeRouterData) -> Result { - Ok(Self { - amount: value.request.amount.to_string(), - currency: value.request.currency.to_string(), - customer: Secret::new(value.get_connector_customer_id()?), - source: Secret::new(value.get_preprocessing_id()?), - }) + { + let order_id = value.connector_request_reference_id.clone(); + let meta_data = Some(get_transaction_metadata( + value.request.metadata.clone(), + order_id, + )); + Ok(Self { + amount: value.request.amount.to_string(), + currency: value.request.currency.to_string(), + customer: Secret::new(value.get_connector_customer_id()?), + source: Secret::new(value.get_preprocessing_id()?), + meta_data, + }) + } } } @@ -3068,6 +3094,7 @@ impl TryFrom #[derive(Debug, Deserialize)] pub struct WebhookEventDataResource { - pub object: serde_json::Value, + pub object: Value, } #[derive(Debug, Deserialize)] @@ -3204,7 +3231,7 @@ pub struct WebhookPaymentMethodDetails { pub payment_method: WebhookPaymentMethodType, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct WebhookEventObjectData { pub id: String, pub object: WebhookEventObjectType, @@ -3219,7 +3246,7 @@ pub struct WebhookEventObjectData { pub metadata: Option, } -#[derive(Debug, Deserialize, strum::Display)] +#[derive(Debug, Clone, Deserialize, strum::Display)] #[serde(rename_all = "snake_case")] pub enum WebhookEventObjectType { PaymentIntent, @@ -3269,7 +3296,7 @@ pub enum WebhookEventType { PaymentIntentProcessing, #[serde(rename = "payment_intent.requires_action")] PaymentIntentRequiresAction, - #[serde(rename = "amount_capturable_updated")] + #[serde(rename = "payment_intent.amount_capturable_updated")] PaymentIntentAmountCapturableUpdated, #[serde(rename = "source.chargeable")] SourceChargeable, @@ -3281,7 +3308,7 @@ pub enum WebhookEventType { Unknown, } -#[derive(Debug, Serialize, strum::Display, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, strum::Display, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum WebhookEventStatus { WarningNeedsResponse, @@ -3305,7 +3332,7 @@ pub enum WebhookEventStatus { Unknown, } -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Debug, Clone, Deserialize, PartialEq)] pub struct EvidenceDetails { #[serde(with = "common_utils::custom_serde::timestamp")] pub due_by: PrimitiveDateTime, @@ -3409,7 +3436,8 @@ impl | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{pm_type:?}"), connector: "Stripe", })?, @@ -3425,26 +3453,16 @@ pub struct StripeGpayToken { pub fn get_bank_transfer_request_data( req: &types::PaymentsAuthorizeRouterData, bank_transfer_data: &api_models::payments::BankTransferData, -) -> CustomResult, errors::ConnectorError> { +) -> CustomResult { match bank_transfer_data { api_models::payments::BankTransferData::AchBankTransfer { .. } | api_models::payments::BankTransferData::MultibancoBankTransfer { .. } => { let req = ChargesRequest::try_from(req)?; - let request = types::RequestBody::log_and_get_request_body( - &req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(request)) + Ok(RequestContent::FormUrlEncoded(Box::new(req))) } _ => { let req = PaymentIntentRequest::try_from(req)?; - let request = types::RequestBody::log_and_get_request_body( - &req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(request)) + Ok(RequestContent::FormUrlEncoded(Box::new(req))) } } } @@ -3556,6 +3574,41 @@ pub struct Evidence { pub submit: bool, } +// Mandates for bank redirects - ideal and sofort happens through sepa direct debit in stripe +fn get_stripe_sepa_dd_mandate_billing_details( + billing_details: &Option, + is_customer_initiated_mandate_payment: Option, +) -> Result { + let billing_name = billing_details + .clone() + .and_then(|billing_data| billing_data.billing_name.clone()); + + let billing_email = billing_details + .clone() + .and_then(|billing_data| billing_data.email.clone()); + match is_customer_initiated_mandate_payment { + Some(true) => Ok(StripeBillingAddress { + name: Some( + billing_name.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "billing_name", + })?, + ), + + email: Some( + billing_email.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "billing_email", + })?, + ), + ..StripeBillingAddress::default() + }), + Some(false) | None => Ok(StripeBillingAddress { + name: billing_name, + email: billing_email, + ..StripeBillingAddress::default() + }), + } +} + impl TryFrom<&types::SubmitEvidenceRouterData> for Evidence { type Error = error_stack::Report; fn try_from(item: &types::SubmitEvidenceRouterData) -> Result { @@ -3601,6 +3654,26 @@ pub struct DisputeObj { pub status: String, } +fn get_transaction_metadata( + merchant_metadata: Option>, + order_id: String, +) -> HashMap { + let mut meta_data = HashMap::from([("metadata[order_id]".to_string(), order_id)]); + let mut request_hash_map = HashMap::new(); + + if let Some(metadata) = merchant_metadata { + let hashmap: HashMap = + serde_json::from_str(&metadata.peek().to_string()).unwrap_or(HashMap::new()); + + for (key, value) in hashmap { + request_hash_map.insert(format!("metadata[{}]", key), value.to_string()); + } + + meta_data.extend(request_hash_map) + }; + meta_data +} + #[cfg(test)] mod test_validate_shipping_address_against_payment_method { #![allow(clippy::unwrap_used)] @@ -3659,7 +3732,10 @@ mod test_validate_shipping_address_against_payment_method { assert!(result.is_err()); let missing_fields = get_missing_fields(result.unwrap_err().current_context()).to_owned(); assert_eq!(missing_fields.len(), 1); - assert_eq!(missing_fields[0], "shipping.address.first_name"); + assert_eq!( + *missing_fields.first().unwrap(), + "shipping.address.first_name" + ); } #[test] @@ -3684,7 +3760,7 @@ mod test_validate_shipping_address_against_payment_method { assert!(result.is_err()); let missing_fields = get_missing_fields(result.unwrap_err().current_context()).to_owned(); assert_eq!(missing_fields.len(), 1); - assert_eq!(missing_fields[0], "shipping.address.line1"); + assert_eq!(*missing_fields.first().unwrap(), "shipping.address.line1"); } #[test] @@ -3709,7 +3785,7 @@ mod test_validate_shipping_address_against_payment_method { assert!(result.is_err()); let missing_fields = get_missing_fields(result.unwrap_err().current_context()).to_owned(); assert_eq!(missing_fields.len(), 1); - assert_eq!(missing_fields[0], "shipping.address.country"); + assert_eq!(*missing_fields.first().unwrap(), "shipping.address.country"); } #[test] @@ -3733,7 +3809,7 @@ mod test_validate_shipping_address_against_payment_method { assert!(result.is_err()); let missing_fields = get_missing_fields(result.unwrap_err().current_context()).to_owned(); assert_eq!(missing_fields.len(), 1); - assert_eq!(missing_fields[0], "shipping.address.zip"); + assert_eq!(*missing_fields.first().unwrap(), "shipping.address.zip"); } #[test] diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 7509131afeef..61ee2e2659d9 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -3,7 +3,9 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; -use common_utils::{crypto, errors::ReportSwitchExt, ext_traits::ByteSliceExt}; +use common_utils::{ + crypto, errors::ReportSwitchExt, ext_traits::ByteSliceExt, request::RequestContent, +}; use error_stack::{IntoReport, Report, ResultExt}; use masking::PeekInterface; use transformers as trustpay; @@ -137,8 +139,11 @@ impl ConnectorCommon for Trustpay { message: option_error_code_message .map(|error_code_message| error_code_message.error_code) .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: reason.or(response_data.description), + reason: reason + .or(response_data.description) + .or(response_data.payment_description), attempt_status: None, + connector_transaction_id: response_data.instance_id, }) } Err(error_msg) => { @@ -149,11 +154,7 @@ impl ConnectorCommon for Trustpay { } } -impl ConnectorValidation for Trustpay { - fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { - Ok(()) - } -} +impl ConnectorValidation for Trustpay {} impl api::Payment for Trustpay {} @@ -177,6 +178,20 @@ impl types::PaymentsResponseData, > for Trustpay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Trustpay".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Trustpay {} @@ -237,14 +252,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = trustpay::TrustpayAuthUpdateRequest::try_from(req)?; - let trustpay_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(trustpay_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -258,7 +268,7 @@ impl ConnectorIntegration CustomResult { - let response: trustpay::TrustPayTransactionStatusErrorResponse = res - .response - .parse_struct("trustpay transaction status ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { - status_code: res.status_code, - code: response.status.to_string(), - // message vary for the same code, so relying on code alone as it is unique - message: response.status.to_string(), - reason: Some(response.payment_description), - attempt_status: None, - }) + self.build_error_response(res) } fn handle_response( @@ -440,28 +440,18 @@ impl &self, req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let currency = req.request.get_currency()?; - let amount = req - .request - .surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.final_amount) - .unwrap_or(req.request.get_amount()?); + let amount = req.request.get_amount()?; let connector_router_data = trustpay::TrustpayRouterData::try_from(( &self.get_currency_unit(), currency, amount, req, ))?; - let create_intent_req = + let connector_req = trustpay::TrustpayCreateIntentRequest::try_from(&connector_router_data)?; - let trustpay_req = types::RequestBody::log_and_get_request_body( - &create_intent_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(trustpay_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -479,7 +469,7 @@ impl .url(&types::PaymentsPreProcessingType::get_url( self, req, connectors, )?) - .body(types::PaymentsPreProcessingType::get_request_body( + .set_body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -558,13 +548,8 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let amount = req - .request - .surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.final_amount) - .unwrap_or(req.request.amount); + ) -> CustomResult { + let amount = req.request.amount; let connector_router_data = trustpay::TrustpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -572,21 +557,12 @@ impl ConnectorIntegration { - types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)? + Ok(RequestContent::Json(Box::new(connector_req))) } - _ => types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - }; - Ok(Some(trustpay_req_string)) + _ => Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))), + } } fn build_request( @@ -604,7 +580,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = trustpay::TrustpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -684,22 +660,12 @@ impl ConnectorIntegration { - types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)? + Ok(RequestContent::Json(Box::new(connector_req))) } - _ => - types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - }; - Ok(Some(trustpay_req_string)) + _ => Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))), + } } fn build_request( @@ -714,7 +680,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( @@ -966,11 +928,15 @@ impl api::IncomingWebhook for Trustpay { .switch()?; let payment_info = trustpay_response.payment_information; let reason = payment_info.status_reason_information.unwrap_or_default(); + let connector_dispute_id = payment_info + .references + .payment_id + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?; Ok(api::disputes::DisputePayload { amount: payment_info.amount.amount.to_string(), currency: payment_info.amount.currency, dispute_stage: api_models::enums::DisputeStage::Dispute, - connector_dispute_id: payment_info.references.payment_id, + connector_dispute_id, connector_reason: reason.reason.reject_reason, connector_reason_code: Some(reason.reason.code), challenge_required_by: None, diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 32b52a115df0..c266753423e9 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -297,9 +297,9 @@ fn get_card_request_data( currency: item.request.currency.to_string(), pan: ccard.card_number.clone(), cvv: ccard.card_cvc.clone(), - expiry_date: ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()), + expiry_date: ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned())?, cardholder: get_full_name(params.billing_first_name, billing_last_name), - reference: item.payment_id.clone(), + reference: item.connector_request_reference_id.clone(), redirect_url: return_url, billing_city: params.billing_city, billing_country: params.billing_country, @@ -377,7 +377,7 @@ fn get_bank_redirection_request_data( currency: item.request.currency.to_string(), }, references: References { - merchant_reference: item.payment_id.clone(), + merchant_reference: item.connector_request_reference_id.clone(), }, debtor: get_debtor_info(item, pm, params)?, }, @@ -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,17 +722,19 @@ fn handle_cards_response( reason: msg, status_code, attempt_status: None, + connector_transaction_id: Some(response.instance_id.clone()), }) } else { None }; let payment_response_data = types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(response.instance_id), + resource_id: types::ResponseId::ConnectorTransactionId(response.instance_id.clone()), redirection_data, mandate_reference: None, 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)) } @@ -802,7 +813,7 @@ fn handle_bank_redirects_sync_response( errors::ConnectorError, > { let status = enums::AttemptStatus::from(response.payment_information.status); - let error = if status == enums::AttemptStatus::AuthorizationFailed { + let error = if utils::is_payment_failure(status) { let reason_info = response .payment_information .status_reason_information @@ -814,25 +825,38 @@ fn handle_bank_redirects_sync_response( reason: reason_info.reason.reject_reason, status_code, attempt_status: None, + connector_transaction_id: Some( + response + .payment_information + .references + .payment_request_id + .clone(), + ), }) } else { None }; let payment_response_data = types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - response.payment_information.references.payment_request_id, + response + .payment_information + .references + .payment_request_id + .clone(), ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payment_response_data)) } pub fn handle_webhook_response( payment_information: WebhookPaymentInformation, + status_code: u16, ) -> CustomResult< ( enums::AttemptStatus, @@ -842,6 +866,22 @@ pub fn handle_webhook_response( errors::ConnectorError, > { let status = enums::AttemptStatus::try_from(payment_information.status)?; + let error = if utils::is_payment_failure(status) { + let reason_info = payment_information + .status_reason_information + .unwrap_or_default(); + Some(types::ErrorResponse { + code: reason_info.reason.code.clone(), + // message vary for the same code, so relying on code alone as it is unique + message: reason_info.reason.code, + reason: reason_info.reason.reject_reason, + status_code, + attempt_status: None, + connector_transaction_id: payment_information.references.payment_request_id.clone(), + }) + } else { + None + }; let payment_response_data = types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::NoResponseId, redirection_data: None, @@ -849,8 +889,9 @@ 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)) + Ok((status, error, payment_response_data)) } pub fn get_trustpay_response( @@ -877,7 +918,9 @@ pub fn get_trustpay_response( TrustpayPaymentsResponse::BankRedirectError(response) => { handle_bank_redirects_error_response(*response, status_code) } - TrustpayPaymentsResponse::WebhookResponse(response) => handle_webhook_response(*response), + TrustpayPaymentsResponse::WebhookResponse(response) => { + handle_webhook_response(*response, status_code) + } } } @@ -941,6 +984,7 @@ impl TryFrom> currency: currency.to_string(), init_apple_pay: is_apple_pay, init_google_pay: is_google_pay, - reference: item.router_data.payment_id.clone(), + reference: item.router_data.connector_request_reference_id.clone(), }) } } @@ -1026,6 +1070,7 @@ pub struct GooglePayTransactionInfo { #[serde(rename_all = "camelCase")] pub struct GooglePayMerchantInfo { pub merchant_name: String, + pub merchant_id: String, } #[derive(Clone, Default, Debug, Deserialize)] @@ -1156,7 +1201,7 @@ pub fn get_apple_pay_session( }, ), payment_request_data: Some(api_models::payments::ApplePayPaymentRequest { - country_code: apple_pay_init_result.country_code, + country_code: Some(apple_pay_init_result.country_code), currency_code: apple_pay_init_result.currency_code, supported_networks: Some(apple_pay_init_result.supported_networks.clone()), merchant_capabilities: Some( @@ -1246,7 +1291,7 @@ impl From for api_models::payments::GpayTransactionInf impl From for api_models::payments::GpayMerchantInfo { fn from(value: GooglePayMerchantInfo) -> Self { Self { - merchant_id: None, + merchant_id: Some(value.merchant_id), merchant_name: value.merchant_name, } } @@ -1413,6 +1458,7 @@ fn handle_cards_refund_response( reason: msg, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -1426,9 +1472,24 @@ fn handle_cards_refund_response( fn handle_webhooks_refund_response( response: WebhookPaymentInformation, + status_code: u16, ) -> CustomResult<(Option, types::RefundsResponseData), errors::ConnectorError> { let refund_status = diesel_models::enums::RefundStatus::try_from(response.status)?; + let error = if utils::is_refund_failure(refund_status) { + let reason_info = response.status_reason_information.unwrap_or_default(); + Some(types::ErrorResponse { + code: reason_info.reason.code.clone(), + // message vary for the same code, so relying on code alone as it is unique + message: reason_info.reason.code, + reason: reason_info.reason.reject_reason, + status_code, + attempt_status: None, + connector_transaction_id: response.references.payment_request_id.clone(), + }) + } else { + None + }; let refund_response_data = types::RefundsResponseData { connector_refund_id: response .references @@ -1436,7 +1497,7 @@ fn handle_webhooks_refund_response( .ok_or(errors::ConnectorError::MissingConnectorRefundID)?, refund_status, }; - Ok((None, refund_response_data)) + Ok((error, refund_response_data)) } fn handle_bank_redirects_refund_response( @@ -1452,6 +1513,7 @@ fn handle_bank_redirects_refund_response( reason: msg.map(|message| message.to_string()), status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -1468,7 +1530,7 @@ fn handle_bank_redirects_refund_sync_response( status_code: u16, ) -> (Option, types::RefundsResponseData) { let refund_status = enums::RefundStatus::from(response.payment_information.status); - let error = if refund_status == enums::RefundStatus::Failure { + let error = if utils::is_refund_failure(refund_status) { let reason_info = response .payment_information .status_reason_information @@ -1480,6 +1542,7 @@ fn handle_bank_redirects_refund_sync_response( reason: reason_info.reason.reject_reason, status_code, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -1502,6 +1565,7 @@ fn handle_bank_redirects_refund_sync_error_response( reason: response.payment_result_info.additional_info, status_code, attempt_status: None, + connector_transaction_id: None, }); //unreachable case as we are sending error as Some() let refund_response_data = types::RefundsResponseData { @@ -1522,7 +1586,9 @@ impl TryFrom> RefundResponse::CardsRefund(response) => { handle_cards_refund_response(*response, item.http_code)? } - RefundResponse::WebhookRefund(response) => handle_webhooks_refund_response(*response)?, + RefundResponse::WebhookRefund(response) => { + handle_webhooks_refund_response(*response, item.http_code)? + } RefundResponse::BankRedirectRefund(response) => { handle_bank_redirects_refund_response(*response, item.http_code) } @@ -1618,16 +1684,13 @@ pub struct Errors { } #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct TrustpayErrorResponse { pub status: i64, pub description: Option, pub errors: Option>, -} - -#[derive(Deserialize)] -pub struct TrustPayTransactionStatusErrorResponse { - pub status: i64, - pub payment_description: String, + pub instance_id: Option, + pub payment_description: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -1674,7 +1737,7 @@ impl TryFrom for diesel_models::enums::RefundStatus { #[serde(rename_all = "PascalCase")] pub struct WebhookReferences { pub merchant_reference: String, - pub payment_id: String, + pub payment_id: Option, pub payment_request_id: Option, } diff --git a/crates/router/src/connector/tsys.rs b/crates/router/src/connector/tsys.rs index 71cef4be2afd..8c19f9f823ae 100644 --- a/crates/router/src/connector/tsys.rs +++ b/crates/router/src/connector/tsys.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use transformers as tsys; @@ -21,7 +22,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -112,6 +113,20 @@ impl types::PaymentsResponseData, > for Tsys { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Tsys".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -144,14 +159,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = tsys::TsysPaymentsRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + ) -> CustomResult { + let connector_req = tsys::TsysPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -169,7 +179,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = tsys::TsysSyncRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + ) -> CustomResult { + let connector_req = tsys::TsysSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -251,7 +256,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = tsys::TsysPaymentsCaptureRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -335,7 +335,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = tsys::TsysPaymentsCancelRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + ) -> CustomResult { + let connector_req = tsys::TsysPaymentsCancelRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -414,7 +409,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = tsys::TsysRefundRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + ) -> CustomResult { + let connector_req = tsys::TsysRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -495,7 +485,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = tsys::TsysSyncRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + ) -> CustomResult { + let connector_req = tsys::TsysSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -575,7 +560,7 @@ impl ConnectorIntegration, - ) -> 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..dd700d11bcbe 100644 --- a/crates/router/src/connector/tsys/transformers.rs +++ b/crates/router/src/connector/tsys/transformers.rs @@ -52,7 +52,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest { currency_code: item.request.currency, card_number: ccard.card_number.clone(), expiration_date: ccard - .get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()), + .get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned())?, cvv2: ccard.card_cvc, terminal_capability: "ICC_CHIP_READ_ONLY".to_string(), terminal_operating_environment: "ON_MERCHANT_PREMISES_ATTENDED".to_string(), @@ -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 efabbf87aeba..87d50bb68a2c 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -10,18 +10,28 @@ use common_utils::{ errors::ReportSwitchExt, pii::{self, Email, IpAddress}, }; +use data_models::payments::payment_attempt::PaymentAttempt; use diesel_models::enums; use error_stack::{report, IntoReport, ResultExt}; use masking::{ExposeInterface, Secret}; use once_cell::sync::Lazy; use regex::Regex; use serde::Serializer; +use time::PrimitiveDateTime; +#[cfg(feature = "frm")] +use crate::types::{fraud_check, storage::enums as storage_enums}; use crate::{ consts, - core::errors::{self, CustomResult}, + core::{ + errors::{self, ApiErrorResponse, CustomResult}, + payments::{PaymentData, RecurringMandatePaymentData}, + }, pii::PeekInterface, - types::{self, api, transformers::ForeignTryFrom, PaymentsCancelData, ResponseId}, + types::{ + self, api, transformers::ForeignTryFrom, ApplePayPredecryptData, BrowserInformation, + PaymentsCancelData, ResponseId, + }, utils::{OptionExt, ValueExt}, }; @@ -59,6 +69,8 @@ pub trait RouterData { fn get_return_url(&self) -> Result; fn get_billing_address(&self) -> Result<&api::AddressDetails, Error>; fn get_shipping_address(&self) -> Result<&api::AddressDetails, Error>; + fn get_billing_address_with_phone_number(&self) -> Result<&api::Address, Error>; + fn get_shipping_address_with_phone_number(&self) -> Result<&api::Address, Error>; fn get_connector_meta(&self) -> Result; fn get_session_token(&self) -> Result; fn to_connector_meta(&self) -> Result @@ -69,11 +81,59 @@ pub trait RouterData { fn get_customer_id(&self) -> Result; fn get_connector_customer_id(&self) -> Result; fn get_preprocessing_id(&self) -> Result; + fn get_recurring_mandate_payment_data(&self) -> Result; #[cfg(feature = "payouts")] fn get_payout_method_data(&self) -> Result; #[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_captured_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 { @@ -121,6 +181,13 @@ impl RouterData for types::RouterData Result<&api::Address, Error> { + self.address + .billing + .as_ref() + .ok_or_else(missing_field_err("billing")) + } fn get_connector_meta(&self) -> Result { self.connector_meta_data .clone() @@ -156,6 +223,14 @@ impl RouterData for types::RouterData Result<&api::Address, Error> { + self.address + .shipping + .as_ref() + .ok_or_else(missing_field_err("shipping")) + } + fn get_payment_method_token(&self) -> Result { self.payment_method_token .clone() @@ -176,6 +251,12 @@ impl RouterData for types::RouterData Result { + self.recurring_mandate_payment_data + .to_owned() + .ok_or_else(missing_field_err("recurring_mandate_payment_data")) + } + #[cfg(feature = "payouts")] fn get_payout_method_data(&self) -> Result { self.payout_method_data @@ -199,7 +280,8 @@ pub trait PaymentsPreProcessingData { fn get_order_details(&self) -> Result, Error>; fn get_webhook_url(&self) -> Result; fn get_return_url(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; + fn get_complete_authorize_url(&self) -> Result; } impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { @@ -239,49 +321,71 @@ impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { .clone() .ok_or_else(missing_field_err("return_url")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) } + fn get_complete_authorize_url(&self) -> Result { + self.complete_authorize_url + .clone() + .ok_or_else(missing_field_err("complete_authorize_url")) + } } pub trait PaymentsCaptureRequestData { fn is_multiple_capture(&self) -> bool; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentsCaptureRequestData for types::PaymentsCaptureData { fn is_multiple_capture(&self) -> bool { self.multiple_capture_data.is_some() } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) } } +pub trait RevokeMandateRequestData { + fn get_connector_mandate_id(&self) -> Result; +} + +impl RevokeMandateRequestData for types::MandateRevokeRequestData { + fn get_connector_mandate_id(&self) -> Result { + self.connector_mandate_id + .clone() + .ok_or_else(missing_field_err("connector_mandate_id")) + } +} + pub trait PaymentsSetupMandateRequestData { - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; + fn get_email(&self) -> Result; } impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .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; fn get_email(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; fn get_order_details(&self) -> Result, Error>; fn get_card(&self) -> Result; fn get_return_url(&self) -> Result; fn connector_mandate_id(&self) -> Option; fn is_mandate_payment(&self) -> bool; + fn is_customer_initiated_mandate_payment(&self) -> bool; fn get_webhook_url(&self) -> Result; fn get_router_return_url(&self) -> Result; fn is_wallet(&self) -> bool; @@ -290,14 +394,18 @@ pub trait PaymentsAuthorizeRequestData { fn get_connector_mandate_id(&self) -> Result; fn get_complete_authorize_url(&self) -> Result; fn get_ip_address_as_optional(&self) -> Option>; + fn get_original_amount(&self) -> i64; + fn get_surcharge_amount(&self) -> Option; + fn get_tax_on_surcharge_amount(&self) -> Option; + fn get_total_surcharge_amount(&self) -> Option; } pub trait PaymentMethodTokenizationRequestData { - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentMethodTokenizationRequestData for types::PaymentMethodTokenizationData { - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -315,7 +423,7 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { fn get_email(&self) -> Result { self.email.clone().ok_or_else(missing_field_err("email")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -396,6 +504,31 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { .map(|ip| Secret::new(ip.to_string())) }) } + fn get_original_amount(&self) -> i64 { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.original_amount) + .unwrap_or(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) + } + fn get_total_surcharge_amount(&self) -> Option { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + } + + fn is_customer_initiated_mandate_payment(&self) -> bool { + self.setup_mandate_details.is_some() + } } pub trait ConnectorCustomerData { @@ -421,7 +554,7 @@ pub trait BrowserInformationData { fn get_ip_address(&self) -> Result, Error>; } -impl BrowserInformationData for types::BrowserInformation { +impl BrowserInformationData for BrowserInformation { fn get_ip_address(&self) -> Result, Error> { let ip_address = self .ip_address @@ -473,6 +606,7 @@ pub trait PaymentsCompleteAuthorizeRequestData { fn is_auto_capture(&self) -> Result; fn get_email(&self) -> Result; fn get_redirect_response_payload(&self) -> Result; + fn get_complete_authorize_url(&self) -> Result; } impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData { @@ -497,6 +631,11 @@ impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData { .into(), ) } + fn get_complete_authorize_url(&self) -> Result { + self.complete_authorize_url + .clone() + .ok_or_else(missing_field_err("complete_authorize_url")) + } } pub trait PaymentsSyncRequestData { @@ -529,7 +668,7 @@ pub trait PaymentsCancelRequestData { fn get_amount(&self) -> Result; fn get_currency(&self) -> Result; fn get_cancellation_reason(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentsCancelRequestData for PaymentsCancelData { @@ -544,7 +683,7 @@ impl PaymentsCancelRequestData for PaymentsCancelData { .clone() .ok_or_else(missing_field_err("cancellation_reason")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -554,7 +693,7 @@ impl PaymentsCancelRequestData for PaymentsCancelData { pub trait RefundsRequestData { fn get_connector_refund_id(&self) -> Result; fn get_webhook_url(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl RefundsRequestData for types::RefundsData { @@ -570,7 +709,7 @@ impl RefundsRequestData for types::RefundsData { .clone() .ok_or_else(missing_field_err("webhook_url")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -655,23 +794,27 @@ pub enum CardIssuer { } pub trait CardData { - fn get_card_expiry_year_2_digit(&self) -> Secret; + fn get_card_expiry_year_2_digit(&self) -> Result, errors::ConnectorError>; fn get_card_issuer(&self) -> Result; fn get_card_expiry_month_year_2_digit_with_delimiter( &self, delimiter: String, - ) -> Secret; + ) -> Result, errors::ConnectorError>; fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret; fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret; fn get_expiry_year_4_digit(&self) -> Secret; - fn get_expiry_date_as_yymm(&self) -> Secret; + fn get_expiry_date_as_yymm(&self) -> Result, errors::ConnectorError>; } impl CardData for api::Card { - fn get_card_expiry_year_2_digit(&self) -> Secret { + fn get_card_expiry_year_2_digit(&self) -> Result, errors::ConnectorError> { let binding = self.card_exp_year.clone(); let year = binding.peek(); - Secret::new(year[year.len() - 2..].to_string()) + Ok(Secret::new( + year.get(year.len() - 2..) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_string(), + )) } fn get_card_issuer(&self) -> Result { get_card_issuer(self.card_number.peek()) @@ -679,14 +822,14 @@ impl CardData for api::Card { fn get_card_expiry_month_year_2_digit_with_delimiter( &self, delimiter: String, - ) -> Secret { - let year = self.get_card_expiry_year_2_digit(); - Secret::new(format!( + ) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?; + Ok(Secret::new(format!( "{}{}{}", - self.card_exp_month.peek().clone(), + self.card_exp_month.peek(), delimiter, year.peek() - )) + ))) } fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret { let year = self.get_expiry_year_4_digit(); @@ -694,14 +837,14 @@ impl CardData for api::Card { "{}{}{}", year.peek(), delimiter, - self.card_exp_month.peek().clone() + self.card_exp_month.peek() )) } fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret { let year = self.get_expiry_year_4_digit(); Secret::new(format!( "{}{}{}", - self.card_exp_month.peek().clone(), + self.card_exp_month.peek(), delimiter, year.peek() )) @@ -713,10 +856,10 @@ impl CardData for api::Card { } Secret::new(year) } - fn get_expiry_date_as_yymm(&self) -> Secret { - let year = self.get_card_expiry_year_2_digit().expose(); + fn get_expiry_date_as_yymm(&self) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?.expose(); let month = self.card_exp_month.clone().expose(); - Secret::new(format!("{year}{month}")) + Ok(Secret::new(format!("{year}{month}"))) } } @@ -796,6 +939,33 @@ impl ApplePay for payments::ApplePayWalletData { } } +pub trait ApplePayDecrypt { + fn get_expiry_month(&self) -> Result, Error>; + fn get_four_digit_expiry_year(&self) -> Result, Error>; +} + +impl ApplePayDecrypt for Box { + fn get_four_digit_expiry_year(&self) -> Result, Error> { + Ok(Secret::new(format!( + "20{}", + self.application_expiration_date + .peek() + .get(0..2) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + ))) + } + + fn get_expiry_month(&self) -> Result, Error> { + Ok(Secret::new( + self.application_expiration_date + .peek() + .get(2..4) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_owned(), + )) + } +} + pub trait CryptoData { fn get_pay_currency(&self) -> Result; } @@ -811,6 +981,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 { @@ -824,6 +995,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 { @@ -964,6 +1140,22 @@ impl MandateData for payments::MandateAmountData { } } +pub trait RecurringMandateData { + fn get_original_payment_amount(&self) -> Result; + fn get_original_payment_currency(&self) -> Result; +} + +impl RecurringMandateData for RecurringMandatePaymentData { + fn get_original_payment_amount(&self) -> Result { + self.original_payment_authorized_amount + .ok_or_else(missing_field_err("original_payment_authorized_amount")) + } + fn get_original_payment_currency(&self) -> Result { + self.original_payment_authorized_currency + .ok_or_else(missing_field_err("original_payment_authorized_currency")) + } +} + pub trait MandateReferenceData { fn get_connector_mandate_id(&self) -> Result; } @@ -1030,23 +1222,6 @@ where json.parse_value(std::any::type_name::()).switch() } -pub fn to_connector_meta_from_secret_with_required_field( - connector_meta: Option>, - error_message: &'static str, -) -> Result -where - T: serde::de::DeserializeOwned, -{ - let connector_error = errors::ConnectorError::MissingRequiredField { - field_name: error_message, - }; - let parsed_meta = to_connector_meta_from_secret(connector_meta).ok(); - match parsed_meta { - Some(meta) => Ok(meta), - _ => Err(connector_error.into()), - } -} - pub fn to_connector_meta_from_secret( connector_meta: Option>, ) -> Result @@ -1055,7 +1230,7 @@ where { let connector_meta_secret = connector_meta.ok_or_else(missing_field_err("connector_meta_data"))?; - let json = connector_meta_secret.peek().clone(); + let json = connector_meta_secret.expose(); json.parse_value(std::any::type_name::()).switch() } @@ -1373,6 +1548,244 @@ pub fn get_error_code_error_message_based_on_priority( .cloned() } +pub trait MultipleCaptureSyncResponse { + fn get_connector_capture_id(&self) -> String; + fn get_capture_attempt_status(&self) -> enums::AttemptStatus; + fn is_capture_response(&self) -> bool; + fn get_connector_reference_id(&self) -> Option { + None + } + fn get_amount_captured(&self) -> Option; +} + +pub fn construct_captures_response_hashmap( + capture_sync_response_list: Vec, +) -> HashMap +where + T: MultipleCaptureSyncResponse, +{ + let mut hashmap = HashMap::new(); + capture_sync_response_list + .into_iter() + .for_each(|capture_sync_response| { + let connector_capture_id = capture_sync_response.get_connector_capture_id(); + if capture_sync_response.is_capture_response() { + hashmap.insert( + connector_capture_id.clone(), + types::CaptureSyncResponse::Success { + resource_id: ResponseId::ConnectorTransactionId(connector_capture_id), + status: capture_sync_response.get_capture_attempt_status(), + connector_response_reference_id: capture_sync_response + .get_connector_reference_id(), + amount: capture_sync_response.get_amount_captured(), + }, + ); + } + }); + hashmap +} + +pub fn is_manual_capture(capture_method: Option) -> bool { + capture_method == Some(enums::CaptureMethod::Manual) + || capture_method == Some(enums::CaptureMethod::ManualMultiple) +} + +pub fn generate_random_bytes(length: usize) -> Vec { + // returns random bytes of length n + let mut rng = rand::thread_rng(); + (0..length).map(|_| rand::Rng::gen(&mut rng)).collect() +} + +pub fn validate_currency( + request_currency: types::storage::enums::Currency, + merchant_config_currency: Option, +) -> Result<(), errors::ConnectorError> { + let merchant_config_currency = + merchant_config_currency.ok_or(errors::ConnectorError::NoConnectorMetaData)?; + if request_currency != merchant_config_currency { + Err(errors::ConnectorError::NotSupported { + message: format!( + "currency {} is not supported for this merchant account", + request_currency + ), + connector: "Braintree", + })? + } + Ok(()) +} + +pub fn get_timestamp_in_milliseconds(datetime: &PrimitiveDateTime) -> i64 { + let utc_datetime = datetime.assume_utc(); + utc_datetime.unix_timestamp() * 1000 +} + +#[cfg(feature = "frm")] +pub trait FraudCheckSaleRequest { + fn get_order_details(&self) -> Result, Error>; +} +#[cfg(feature = "frm")] +impl FraudCheckSaleRequest for fraud_check::FraudCheckSaleData { + fn get_order_details(&self) -> Result, Error> { + self.order_details + .clone() + .ok_or_else(missing_field_err("order_details")) + } +} + +#[cfg(feature = "frm")] +pub trait FraudCheckCheckoutRequest { + fn get_order_details(&self) -> Result, Error>; +} +#[cfg(feature = "frm")] +impl FraudCheckCheckoutRequest for fraud_check::FraudCheckCheckoutData { + fn get_order_details(&self) -> Result, Error> { + self.order_details + .clone() + .ok_or_else(missing_field_err("order_details")) + } +} + +#[cfg(feature = "frm")] +pub trait FraudCheckTransactionRequest { + fn get_currency(&self) -> Result; +} +#[cfg(feature = "frm")] +impl FraudCheckTransactionRequest for fraud_check::FraudCheckTransactionData { + fn get_currency(&self) -> Result { + self.currency.ok_or_else(missing_field_err("currency")) + } +} + +#[cfg(feature = "frm")] +pub trait FraudCheckRecordReturnRequest { + fn get_currency(&self) -> Result; +} +#[cfg(feature = "frm")] +impl FraudCheckRecordReturnRequest for fraud_check::FraudCheckRecordReturnData { + fn get_currency(&self) -> Result { + self.currency.ok_or_else(missing_field_err("currency")) + } +} + +pub trait AccessPaymentAttemptInfo { + fn get_browser_info( + &self, + ) -> Result, error_stack::Report>; +} + +impl AccessPaymentAttemptInfo for PaymentAttempt { + fn get_browser_info( + &self, + ) -> Result, error_stack::Report> { + self.browser_info + .clone() + .map(|b| b.parse_value("BrowserInformation")) + .transpose() + .change_context(ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + }) + } +} + +pub trait PaymentsAttemptData { + fn get_browser_info(&self) + -> Result>; +} + +impl PaymentsAttemptData for PaymentAttempt { + fn get_browser_info( + &self, + ) -> Result> { + self.browser_info + .clone() + .ok_or(ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + })? + .parse_value::("BrowserInformation") + .change_context(ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + }) + } +} + +#[cfg(feature = "frm")] +pub trait FrmTransactionRouterDataRequest { + fn is_payment_successful(&self) -> Option; +} + +#[cfg(feature = "frm")] +impl FrmTransactionRouterDataRequest for fraud_check::FrmTransactionRouterData { + fn is_payment_successful(&self) -> Option { + match self.status { + storage_enums::AttemptStatus::AuthenticationFailed + | storage_enums::AttemptStatus::RouterDeclined + | storage_enums::AttemptStatus::AuthorizationFailed + | storage_enums::AttemptStatus::Voided + | storage_enums::AttemptStatus::CaptureFailed + | storage_enums::AttemptStatus::Failure + | storage_enums::AttemptStatus::AutoRefunded => Some(false), + + storage_enums::AttemptStatus::AuthenticationSuccessful + | storage_enums::AttemptStatus::PartialChargedAndChargeable + | storage_enums::AttemptStatus::Authorized + | storage_enums::AttemptStatus::Charged => Some(true), + + storage_enums::AttemptStatus::Started + | storage_enums::AttemptStatus::AuthenticationPending + | storage_enums::AttemptStatus::Authorizing + | storage_enums::AttemptStatus::CodInitiated + | storage_enums::AttemptStatus::VoidInitiated + | storage_enums::AttemptStatus::CaptureInitiated + | storage_enums::AttemptStatus::VoidFailed + | storage_enums::AttemptStatus::PartialCharged + | storage_enums::AttemptStatus::Unresolved + | storage_enums::AttemptStatus::Pending + | storage_enums::AttemptStatus::PaymentMethodAwaited + | storage_enums::AttemptStatus::ConfirmationAwaited + | storage_enums::AttemptStatus::DeviceDataCollectionPending => None, + } + } +} + +pub fn is_payment_failure(status: enums::AttemptStatus) -> bool { + match status { + common_enums::AttemptStatus::AuthenticationFailed + | common_enums::AttemptStatus::AuthorizationFailed + | common_enums::AttemptStatus::CaptureFailed + | common_enums::AttemptStatus::VoidFailed + | common_enums::AttemptStatus::Failure => true, + common_enums::AttemptStatus::Started + | common_enums::AttemptStatus::RouterDeclined + | common_enums::AttemptStatus::AuthenticationPending + | common_enums::AttemptStatus::AuthenticationSuccessful + | common_enums::AttemptStatus::Authorized + | common_enums::AttemptStatus::Charged + | common_enums::AttemptStatus::Authorizing + | common_enums::AttemptStatus::CodInitiated + | common_enums::AttemptStatus::Voided + | common_enums::AttemptStatus::VoidInitiated + | common_enums::AttemptStatus::CaptureInitiated + | common_enums::AttemptStatus::AutoRefunded + | common_enums::AttemptStatus::PartialCharged + | common_enums::AttemptStatus::PartialChargedAndChargeable + | common_enums::AttemptStatus::Unresolved + | common_enums::AttemptStatus::Pending + | common_enums::AttemptStatus::PaymentMethodAwaited + | common_enums::AttemptStatus::ConfirmationAwaited + | common_enums::AttemptStatus::DeviceDataCollectionPending => false, + } +} + +pub fn is_refund_failure(status: enums::RefundStatus) -> bool { + match status { + common_enums::RefundStatus::Failure | common_enums::RefundStatus::TransactionFailure => { + true + } + common_enums::RefundStatus::ManualReview + | common_enums::RefundStatus::Pending + | common_enums::RefundStatus::Success => false, + } +} #[cfg(test)] mod error_code_error_message_tests { #![allow(clippy::unwrap_used)] @@ -1452,63 +1865,3 @@ mod error_code_error_message_tests { assert_eq!(error_code_error_message_none, None); } } - -pub trait MultipleCaptureSyncResponse { - fn get_connector_capture_id(&self) -> String; - fn get_capture_attempt_status(&self) -> enums::AttemptStatus; - fn is_capture_response(&self) -> bool; - fn get_connector_reference_id(&self) -> Option { - None - } - fn get_amount_captured(&self) -> Option; -} - -pub fn construct_captures_response_hashmap( - capture_sync_response_list: Vec, -) -> HashMap -where - T: MultipleCaptureSyncResponse, -{ - let mut hashmap = HashMap::new(); - capture_sync_response_list - .into_iter() - .for_each(|capture_sync_response| { - let connector_capture_id = capture_sync_response.get_connector_capture_id(); - if capture_sync_response.is_capture_response() { - hashmap.insert( - connector_capture_id.clone(), - types::CaptureSyncResponse::Success { - resource_id: ResponseId::ConnectorTransactionId(connector_capture_id), - status: capture_sync_response.get_capture_attempt_status(), - connector_response_reference_id: capture_sync_response - .get_connector_reference_id(), - amount: capture_sync_response.get_amount_captured(), - }, - ); - } - }); - hashmap -} - -pub fn is_manual_capture(capture_method: Option) -> bool { - capture_method == Some(enums::CaptureMethod::Manual) - || capture_method == Some(enums::CaptureMethod::ManualMultiple) -} - -pub fn validate_currency( - request_currency: types::storage::enums::Currency, - merchant_config_currency: Option, -) -> Result<(), errors::ConnectorError> { - let merchant_config_currency = - merchant_config_currency.ok_or(errors::ConnectorError::NoConnectorMetaData)?; - if request_currency != merchant_config_currency { - Err(errors::ConnectorError::NotSupported { - message: format!( - "currency {} is not supported for this merchant account", - request_currency - ), - connector: "Braintree", - })? - } - Ok(()) -} diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index 3697b8c8923f..f125f90d93aa 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -2,10 +2,13 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; use transformers as volt; +use self::transformers::webhook_headers; +use super::utils; use crate::{ configs::settings, core::errors::{self, CustomResult}, @@ -20,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -130,6 +133,7 @@ impl ConnectorCommon for Volt { message: response.exception.message.clone(), reason: Some(reason), attempt_status: None, + connector_transaction_id: None, }) } } @@ -175,15 +179,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = volt::VoltAuthUpdateRequest::try_from(req)?; - let volt_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = volt::VoltAuthUpdateRequest::try_from(req)?; - Ok(Some(volt_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -197,7 +196,7 @@ impl ConnectorIntegration CustomResult { - self.build_error_response(res) + // auth error have different structure than common error + let response: volt::VoltAuthErrorResponse = res + .response + .parse_struct("VoltAuthErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code.to_string(), + message: response.message.clone(), + reason: Some(response.message), + attempt_status: None, + connector_transaction_id: None, + }) } } @@ -237,6 +249,20 @@ impl types::PaymentsResponseData, > for Volt { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Volt".to_string()) + .into(), + ) + } } impl ConnectorIntegration @@ -266,20 +292,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = volt::VoltRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = volt::VoltPaymentsRequest::try_from(&connector_router_data)?; - let volt_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(volt_req)) + let connector_req = volt::VoltPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -297,7 +318,7 @@ impl ConnectorIntegration CustomResult { - let response: volt::VoltPsyncResponse = res + let response: volt::VoltPaymentsResponseData = res .response .parse_struct("volt PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -425,7 +446,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -442,7 +463,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = volt::VoltRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = volt::VoltRefundRequest::try_from(&connector_router_data)?; - let volt_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(volt_req)) + let connector_req = volt::VoltRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -535,7 +551,7 @@ impl ConnectorIntegration, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let signature = + utils::get_header_key_value(webhook_headers::X_VOLT_SIGNED, request.headers) + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + + hex::decode(signature) + .into_report() + .change_context(errors::ConnectorError::WebhookVerificationSecretInvalid) + } + + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let x_volt_timed = + utils::get_header_key_value(webhook_headers::X_VOLT_TIMED, request.headers)?; + let user_agent = utils::get_header_key_value(webhook_headers::USER_AGENT, request.headers)?; + let version = user_agent + .split('/') + .last() + .ok_or(errors::ConnectorError::WebhookSourceVerificationFailed)?; + Ok(format!( + "{}|{}|{}", + String::from_utf8_lossy(request.body), + x_volt_timed, + version + ) + .into_bytes()) + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let parsed_webhook_response = request + .body + .parse_struct::("VoltRefundWebhookBodyReference") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + + match parsed_webhook_response { + volt::WebhookResponse::Payment(payment_response) => { + let reference = match payment_response.merchant_internal_reference { + Some(merchant_internal_reference) => { + api_models::payments::PaymentIdType::PaymentAttemptId( + merchant_internal_reference, + ) + } + None => api_models::payments::PaymentIdType::ConnectorTransactionId( + payment_response.payment, + ), + }; + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + reference, + )) + } + volt::WebhookResponse::Refund(refund_response) => { + let refund_reference = match refund_response.external_reference { + Some(external_reference) => { + api_models::webhooks::RefundIdType::RefundId(external_reference) + } + None => api_models::webhooks::RefundIdType::ConnectorRefundId( + refund_response.refund, + ), + }; + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + refund_reference, + )) + } + } } fn get_webhook_event_type( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + if request.body.is_empty() { + Ok(api::IncomingWebhookEvent::EndpointVerification) + } else { + let payload: volt::VoltWebhookBodyEventType = request + .body + .parse_struct("VoltWebhookBodyEventType") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + Ok(api::IncomingWebhookEvent::from(payload)) + } } fn get_webhook_resource_object( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + let details: volt::VoltWebhookObjectResource = request + .body + .parse_struct("VoltWebhookObjectResource") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + Ok(Box::new(details)) } } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index e603ef2db06c..b1d77f3416ff 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{self, AddressDetailsData, RouterData}, + consts, core::errors, services, types::{self, api, storage::enums as storage_enums}, @@ -41,6 +42,12 @@ impl } } +pub mod webhook_headers { + pub const X_VOLT_SIGNED: &str = "X-Volt-Signed"; + pub const X_VOLT_TIMED: &str = "X-Volt-Timed"; + pub const USER_AGENT: &str = "User-Agent"; +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct VoltPaymentsRequest { @@ -50,7 +57,6 @@ pub struct VoltPaymentsRequest { transaction_type: TransactionType, merchant_internal_reference: String, shopper: ShopperDetails, - notification_url: Option, payment_success_url: Option, payment_failure_url: Option, payment_pending_url: Option, @@ -91,7 +97,6 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme let payment_failure_url = item.router_data.request.router_return_url.clone(); let payment_pending_url = item.router_data.request.router_return_url.clone(); let payment_cancel_url = item.router_data.request.router_return_url.clone(); - let notification_url = item.router_data.request.webhook_url.clone(); let address = item.router_data.get_billing_address()?; let shopper = ShopperDetails { email: item.router_data.request.email.clone(), @@ -109,7 +114,6 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme payment_failure_url, payment_pending_url, payment_cancel_url, - notification_url, shopper, transaction_type, }) @@ -130,10 +134,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 +151,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,14 +288,16 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Clone, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[derive(strum::Display)] pub enum VoltPaymentStatus { NewPayment, Completed, @@ -309,7 +314,15 @@ pub enum VoltPaymentStatus { Failed, Settled, } -#[derive(Debug, Deserialize)] + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum VoltPaymentsResponseData { + PsyncResponse(VoltPsyncResponse), + WebhookResponse(VoltPaymentWebhookObjectResource), +} + +#[derive(Debug, Serialize, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct VoltPsyncResponse { status: VoltPaymentStatus, @@ -317,34 +330,112 @@ pub struct VoltPsyncResponse { merchant_internal_reference: Option, } -impl TryFrom> +impl + TryFrom> for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::ResponseRouterData, + item: types::ResponseRouterData< + F, + VoltPaymentsResponseData, + T, + types::PaymentsResponseData, + >, ) -> Result { - Ok(Self { - status: enums::AttemptStatus::from(item.response.status), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: item - .response - .merchant_internal_reference - .or(Some(item.response.id)), - }), - ..item.data - }) + match item.response { + VoltPaymentsResponseData::PsyncResponse(payment_response) => { + let status = enums::AttemptStatus::from(payment_response.status.clone()); + Ok(Self { + status, + response: if is_payment_failure(status) { + Err(types::ErrorResponse { + code: payment_response.status.clone().to_string(), + message: payment_response.status.clone().to_string(), + reason: Some(payment_response.status.to_string()), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(payment_response.id), + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + payment_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: payment_response + .merchant_internal_reference + .or(Some(payment_response.id)), + incremental_authorization_allowed: None, + }) + }, + ..item.data + }) + } + VoltPaymentsResponseData::WebhookResponse(webhook_response) => { + let detailed_status = webhook_response.detailed_status.clone(); + let status = enums::AttemptStatus::from(webhook_response.status); + Ok(Self { + status, + response: if is_payment_failure(status) { + Err(types::ErrorResponse { + code: detailed_status + .clone() + .map(|volt_status| volt_status.to_string()) + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_owned()), + message: detailed_status + .clone() + .map(|volt_status| volt_status.to_string()) + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_owned()), + reason: detailed_status + .clone() + .map(|volt_status| volt_status.to_string()), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(webhook_response.payment.clone()), + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + webhook_response.payment.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: webhook_response + .merchant_internal_reference + .or(Some(webhook_response.payment)), + incremental_authorization_allowed: None, + }) + }, + ..item.data + }) + } + } + } +} +impl From for enums::AttemptStatus { + fn from(status: VoltWebhookPaymentStatus) -> Self { + match status { + VoltWebhookPaymentStatus::Completed | VoltWebhookPaymentStatus::Received => { + Self::Charged + } + VoltWebhookPaymentStatus::Failed | VoltWebhookPaymentStatus::NotReceived => { + Self::Failure + } + VoltWebhookPaymentStatus::Pending => Self::Pending, + } } } // REFUND : // Type definition for RefundRequest #[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct VoltRefundRequest { pub amount: i64, pub external_reference: String, @@ -360,28 +451,6 @@ impl TryFrom<&VoltRouterData<&types::RefundsRouterData>> for VoltRefundReq } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping - } - } -} - #[derive(Default, Debug, Clone, Deserialize)] pub struct RefundResponse { id: String, @@ -404,11 +473,137 @@ impl TryFrom> } } +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltPaymentWebhookBodyReference { + pub payment: String, + pub merchant_internal_reference: Option, +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltRefundWebhookBodyReference { + pub refund: String, + pub external_reference: Option, +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum WebhookResponse { + // the enum order shouldn't be changed as this is being used during serialization and deserialization + Refund(VoltRefundWebhookBodyReference), + Payment(VoltPaymentWebhookBodyReference), +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum VoltWebhookBodyEventType { + Payment(VoltPaymentsWebhookBodyEventType), + Refund(VoltRefundsWebhookBodyEventType), +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltPaymentsWebhookBodyEventType { + pub status: VoltWebhookPaymentStatus, + pub detailed_status: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltRefundsWebhookBodyEventType { + pub status: VoltWebhookRefundsStatus, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum VoltWebhookObjectResource { + Payment(VoltPaymentWebhookObjectResource), + Refund(VoltRefundWebhookObjectResource), +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltPaymentWebhookObjectResource { + pub payment: String, + pub merchant_internal_reference: Option, + pub status: VoltWebhookPaymentStatus, + pub detailed_status: Option, +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltRefundWebhookObjectResource { + pub refund: String, + pub external_reference: Option, + pub status: VoltWebhookRefundsStatus, +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VoltWebhookPaymentStatus { + Completed, + Failed, + Pending, + Received, + NotReceived, +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VoltWebhookRefundsStatus { + RefundConfirmed, + RefundFailed, +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[derive(strum::Display)] +pub enum VoltDetailedStatus { + RefusedByRisk, + RefusedByBank, + ErrorAtBank, + CancelledByUser, + AbandonedByUser, + Failed, + Completed, + BankRedirect, + DelayedAtBank, + AwaitingCheckoutAuthorisation, +} + +impl From for api::IncomingWebhookEvent { + fn from(status: VoltWebhookBodyEventType) -> Self { + match status { + VoltWebhookBodyEventType::Payment(payment_data) => match payment_data.status { + VoltWebhookPaymentStatus::Completed | VoltWebhookPaymentStatus::Received => { + Self::PaymentIntentSuccess + } + VoltWebhookPaymentStatus::Failed | VoltWebhookPaymentStatus::NotReceived => { + Self::PaymentIntentFailure + } + VoltWebhookPaymentStatus::Pending => Self::PaymentIntentProcessing, + }, + VoltWebhookBodyEventType::Refund(refund_data) => match refund_data.status { + VoltWebhookRefundsStatus::RefundConfirmed => Self::RefundSuccess, + VoltWebhookRefundsStatus::RefundFailed => Self::RefundFailure, + }, + } + } +} + #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] pub struct VoltErrorResponse { pub exception: VoltErrorException, } +#[derive(Debug, Deserialize)] +pub struct VoltAuthErrorResponse { + pub code: u64, + pub message: String, +} + #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct VoltErrorException { @@ -422,3 +617,32 @@ pub struct VoltErrorList { pub property: String, pub message: String, } + +fn is_payment_failure(status: enums::AttemptStatus) -> bool { + match status { + common_enums::AttemptStatus::AuthenticationFailed + | common_enums::AttemptStatus::AuthorizationFailed + | common_enums::AttemptStatus::CaptureFailed + | common_enums::AttemptStatus::VoidFailed + | common_enums::AttemptStatus::Failure => true, + common_enums::AttemptStatus::Started + | common_enums::AttemptStatus::RouterDeclined + | common_enums::AttemptStatus::AuthenticationPending + | common_enums::AttemptStatus::AuthenticationSuccessful + | common_enums::AttemptStatus::Authorized + | common_enums::AttemptStatus::Charged + | common_enums::AttemptStatus::Authorizing + | common_enums::AttemptStatus::CodInitiated + | common_enums::AttemptStatus::Voided + | common_enums::AttemptStatus::VoidInitiated + | common_enums::AttemptStatus::CaptureInitiated + | common_enums::AttemptStatus::AutoRefunded + | common_enums::AttemptStatus::PartialCharged + | common_enums::AttemptStatus::PartialChargedAndChargeable + | common_enums::AttemptStatus::Unresolved + | common_enums::AttemptStatus::Pending + | common_enums::AttemptStatus::PaymentMethodAwaited + | common_enums::AttemptStatus::ConfirmationAwaited + | common_enums::AttemptStatus::DeviceDataCollectionPending => false, + } +} diff --git a/crates/router/src/connector/wise.rs b/crates/router/src/connector/wise.rs index 5eba54eab4f7..66ccf22c100d 100644 --- a/crates/router/src/connector/wise.rs +++ b/crates/router/src/connector/wise.rs @@ -1,6 +1,8 @@ pub mod transformers; use std::fmt::Debug; +#[cfg(feature = "payouts")] +use common_utils::request::RequestContent; use error_stack::{IntoReport, ResultExt}; #[cfg(feature = "payouts")] use masking::PeekInterface; @@ -24,7 +26,7 @@ use crate::{ utils::BytesExt, }; #[cfg(feature = "payouts")] -use crate::{core::payments, routes, utils}; +use crate::{core::payments, routes}; #[derive(Debug, Clone)] pub struct Wise; @@ -88,13 +90,14 @@ impl ConnectorCommon for Wise { let default_status = response.status.unwrap_or_default().to_string(); match response.errors { Some(errs) => { - if let Some(e) = errs.get(0) { + if let Some(e) = errs.first() { Ok(types::ErrorResponse { status_code: res.status_code, code: e.code.clone(), message: e.message.clone(), reason: None, attempt_status: None, + connector_transaction_id: None, }) } else { Ok(types::ErrorResponse { @@ -103,6 +106,7 @@ impl ConnectorCommon for Wise { message: response.message.unwrap_or_default(), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -112,6 +116,7 @@ impl ConnectorCommon for Wise { message: response.message.unwrap_or_default(), reason: None, attempt_status: None, + connector_transaction_id: None, }), } } @@ -152,6 +157,20 @@ impl types::PaymentsResponseData, > for Wise { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Wise".to_string()) + .into(), + ) + } } impl api::PaymentSession for Wise {} @@ -282,7 +301,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = wise::WisePayoutQuoteRequest::try_from(req)?; - let wise_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(wise_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -347,7 +362,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = wise::WiseRecipientCreateRequest::try_from(req)?; - let wise_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(wise_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -427,7 +437,7 @@ impl .headers(types::PayoutRecipientType::get_headers( self, req, connectors, )?) - .body(types::PayoutRecipientType::get_request_body( + .set_body(types::PayoutRecipientType::get_request_body( self, req, connectors, )?) .build(); @@ -523,14 +533,9 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = wise::WisePayoutCreateRequest::try_from(req)?; - let wise_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(wise_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -543,7 +548,7 @@ impl services::ConnectorIntegration for Wise { + fn build_request( + &self, + _req: &types::PayoutsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + // Eligiblity check for cards is not implemented + Err( + errors::ConnectorError::NotImplemented("Payout Eligibility for Wise".to_string()) + .into(), + ) + } } #[cfg(feature = "payouts")] @@ -622,14 +638,9 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = wise::WisePayoutFulfillRequest::try_from(req)?; - let wise_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(wise_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -644,7 +655,7 @@ impl services::ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/wise/transformers.rs b/crates/router/src/connector/wise/transformers.rs index c481f0c73433..e0fc05c3c89e 100644 --- a/crates/router/src/connector/wise/transformers.rs +++ b/crates/router/src/connector/wise/transformers.rs @@ -11,7 +11,7 @@ type Error = error_stack::Report; #[cfg(feature = "payouts")] use crate::{ - connector::utils::RouterData, + connector::utils::{self, RouterData}, types::{ api::payouts, storage::enums::{self as storage_enums, PayoutEntityType}, @@ -344,10 +344,9 @@ fn get_payout_bank_details( bic: b.bic, ..WiseBankDetails::default() }), - _ => Err(errors::ConnectorError::NotSupported { - message: "Card payout creation is not supported".to_string(), - connector: "Wise", - }), + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ))?, } } @@ -371,10 +370,9 @@ impl TryFrom<&types::PayoutsRouterData> for WiseRecipientCreateRequest { }), }?; match request.payout_type.to_owned() { - storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotSupported { - message: "Card payout creation is not supported".to_string(), - connector: "Wise", - })?, + storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ))?, storage_enums::PayoutType::Bank => { let account_holder_name = customer_details .ok_or(errors::ConnectorError::MissingRequiredField { @@ -432,10 +430,9 @@ impl TryFrom<&types::PayoutsRouterData> for WisePayoutQuoteRequest { target_currency: request.destination_currency.to_string(), pay_out: WisePayOutOption::default(), }), - storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotSupported { - message: "Card payout fulfillment is not supported".to_string(), - connector: "Wise", - })?, + storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ))?, } } } @@ -489,10 +486,9 @@ impl TryFrom<&types::PayoutsRouterData> for WisePayoutCreateRequest { details: wise_transfer_details, }) } - storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotSupported { - message: "Card payout fulfillment is not supported".to_string(), - connector: "Wise", - })?, + storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ))?, } } } @@ -533,10 +529,9 @@ impl TryFrom<&types::PayoutsRouterData> for WisePayoutFulfillRequest { storage_enums::PayoutType::Bank => Ok(Self { fund_type: FundType::default(), }), - storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotSupported { - message: "Card payout fulfillment is not supported".to_string(), - connector: "Wise", - })?, + storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ))?, } } } @@ -599,10 +594,9 @@ impl TryFrom for RecipientType { PayoutMethodData::Bank(api_models::payouts::Bank::Ach(_)) => Ok(Self::Aba), PayoutMethodData::Bank(api_models::payouts::Bank::Bacs(_)) => Ok(Self::SortCode), PayoutMethodData::Bank(api_models::payouts::Bank::Sepa(_)) => Ok(Self::Iban), - _ => Err(errors::ConnectorError::NotSupported { - message: "Requested payout_method_type is not supported".to_string(), - connector: "Wise", - } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ) .into()), } } diff --git a/crates/router/src/connector/worldline.rs b/crates/router/src/connector/worldline.rs index 7fcca08d8bfe..a1ca8a110bc1 100644 --- a/crates/router/src/connector/worldline.rs +++ b/crates/router/src/connector/worldline.rs @@ -3,7 +3,7 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; -use common_utils::ext_traits::ByteSliceExt; +use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; @@ -11,10 +11,9 @@ use ring::hmac; use time::{format_description, OffsetDateTime}; use transformers as worldline; -use super::utils::RefundsRequestData; +use super::utils::{self as connector_utils, RefundsRequestData}; use crate::{ configs::settings::Connectors, - connector::{utils as connector_utils, utils as conn_utils}, consts, core::errors::{self, CustomResult}, headers, logger, @@ -28,7 +27,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, }, - utils::{self, crypto, BytesExt, OptionExt}, + utils::{crypto, BytesExt, OptionExt}, }; #[derive(Debug, Clone)] @@ -174,6 +173,20 @@ impl types::PaymentsResponseData, > for Worldline { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Worldline".to_string()) + .into(), + ) + } } impl api::PaymentToken for Worldline {} @@ -379,15 +392,10 @@ impl ConnectorIntegration, _connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = worldline::ApproveRequest::try_from(req)?; - let worldline_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(worldline_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -407,7 +415,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = worldline::WorldlineRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -501,12 +509,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(worldline_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -528,7 +531,7 @@ impl ConnectorIntegration, _connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = worldline::WorldlineRefundRequest::try_from(req)?; - let refund_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(refund_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -617,7 +615,7 @@ impl ConnectorIntegration, _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let header_value = conn_utils::get_header_key_value("X-GCS-Signature", request.headers)?; + let header_value = + connector_utils::get_header_key_value("X-GCS-Signature", request.headers)?; let signature = consts::BASE64_ENGINE .decode(header_value.as_bytes()) .into_report() @@ -808,14 +807,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..c55663d59f48 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()), } } @@ -342,16 +342,21 @@ fn make_card_request( req: &PaymentsAuthorizeData, ccard: &payments::Card, ) -> Result> { - let expiry_year = ccard.card_exp_year.peek().clone(); + let expiry_year = ccard.card_exp_year.peek(); let secret_value = format!( "{}{}", ccard.card_exp_month.peek(), - &expiry_year[expiry_year.len() - 2..] + &expiry_year + .get(expiry_year.len() - 2..) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? ); let expiry_date: Secret = Secret::new(secret_value); let card = Card { card_number: ccard.card_number.clone(), - cardholder_name: ccard.card_holder_name.clone(), + cardholder_name: ccard + .card_holder_name + .clone() + .unwrap_or(Secret::new("".to_string())), cvv: ccard.card_cvc.clone(), expiry_date, }; @@ -594,6 +599,7 @@ impl TryFrom TryFrom for Worldpay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Worldpay".to_string()) + .into(), + ) + } } impl api::PaymentToken for Worldpay {} @@ -208,6 +223,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = worldpay::WorldpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = WorldpayPaymentsRequest::try_from(&connector_router_data)?; - let worldpay_payment_request = types::RequestBody::log_and_get_request_body( - &connector_request, - ext_traits::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(worldpay_payment_request)) + let connector_req = WorldpayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -466,7 +476,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = WorldpayRefundRequest::try_from(req)?; - let fiserv_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - ext_traits::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_refund_request)) + ) -> CustomResult { + let connector_req = WorldpayRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn get_url( @@ -556,7 +561,7 @@ impl ConnectorIntegration, - ) -> 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..0d5634c40c3c 100644 --- a/crates/router/src/connector/zen.rs +++ b/crates/router/src/connector/zen.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::{crypto, ext_traits::ByteSliceExt}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as zen; @@ -27,7 +27,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, domain, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -127,6 +127,7 @@ impl ConnectorCommon for Zen { ), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -169,6 +170,17 @@ impl types::PaymentsResponseData, > for Zen { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("Setup Mandate flow for Zen".to_string()).into()) + } } impl ConnectorIntegration @@ -217,20 +229,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = zen::ZenRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = zen::ZenPaymentsRequest::try_from(&connector_router_data)?; - let zen_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(zen_req)) + let connector_req = zen::ZenPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -248,7 +255,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = zen::ZenRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = zen::ZenRefundRequest::try_from(&connector_router_data)?; - let zen_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(zen_req)) + let connector_req = zen::ZenRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -443,7 +445,7 @@ impl ConnectorIntegration, - ) -> 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 689894176b26..0adae0d00bdb 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -11,6 +11,7 @@ use crate::{ connector::utils::{ self, BrowserInformationData, CardData, PaymentsAuthorizeRequestData, RouterData, }, + consts, core::errors::{self, CustomResult}, services::{self, Method}, types::{self, api, storage::enums, transformers::ForeignTryFrom}, @@ -222,7 +223,7 @@ impl TryFrom<(&ZenRouterData<&types::PaymentsAuthorizeRouterData>, &Card)> for Z card: Some(ZenCardDetails { number: ccard.card_number.clone(), expiry_date: ccard - .get_card_expiry_month_year_2_digit_with_delimiter("".to_owned()), + .get_card_expiry_month_year_2_digit_with_delimiter("".to_owned())?, cvv: ccard.card_cvc.clone(), }), descriptor: item @@ -538,7 +539,7 @@ fn get_checkout_signature( .pay_wall_secret .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let mut signature_data = get_signature_data(checkout_request); + let mut signature_data = get_signature_data(checkout_request)?; signature_data.push_str(&pay_wall_secret); let payload_digest = digest::digest(&digest::SHA256, signature_data.as_bytes()); let mut signature = hex::encode(payload_digest); @@ -547,7 +548,9 @@ fn get_checkout_signature( } /// Fields should be in alphabetical order -fn get_signature_data(checkout_request: &CheckoutRequest) -> String { +fn get_signature_data( + checkout_request: &CheckoutRequest, +) -> Result { let specified_payment_channel = match checkout_request.specified_payment_channel { ZenPaymentChannels::PclCard => "pcl_card", ZenPaymentChannels::PclGooglepay => "pcl_googlepay", @@ -568,21 +571,19 @@ fn get_signature_data(checkout_request: &CheckoutRequest) -> String { ]; for index in 0..checkout_request.items.len() { let prefix = format!("items[{index}]."); + let checkout_request_items = checkout_request + .items + .get(index) + .ok_or(errors::ConnectorError::RequestEncodingFailed)?; signature_data.push(format!( "{prefix}lineamounttotal={}", - checkout_request.items[index].line_amount_total - )); - signature_data.push(format!( - "{prefix}name={}", - checkout_request.items[index].name - )); - signature_data.push(format!( - "{prefix}price={}", - checkout_request.items[index].price + checkout_request_items.line_amount_total )); + signature_data.push(format!("{prefix}name={}", checkout_request_items.name)); + signature_data.push(format!("{prefix}price={}", checkout_request_items.price)); signature_data.push(format!( "{prefix}quantity={}", - checkout_request.items[index].quantity + checkout_request_items.quantity )); } signature_data.push(format!( @@ -598,7 +599,7 @@ fn get_signature_data(checkout_request: &CheckoutRequest) -> String { )); signature_data.push(format!("urlredirect={}", checkout_request.url_redirect)); let signature = signature_data.join("&"); - signature.to_lowercase() + Ok(signature.to_lowercase()) } fn get_customer( @@ -707,7 +708,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"), ))? @@ -847,12 +849,15 @@ impl ForeignTryFrom<(ZenPaymentStatus, Option)> for enums::AttemptSt } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApiResponse { status: ZenPaymentStatus, id: String, + // merchant_transaction_id: Option, merchant_action: Option, + reject_code: Option, + reject_reason: Option, } #[derive(Debug, Deserialize)] @@ -868,18 +873,18 @@ pub struct CheckoutResponse { redirect_url: url::Url, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ZenMerchantAction { action: ZenActions, data: ZenMerchantActionData, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum ZenActions { Redirect, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ZenMerchantActionData { redirect_url: url::Url, @@ -912,6 +917,57 @@ impl } } +fn get_zen_response( + response: ApiResponse, + status_code: u16, +) -> CustomResult< + ( + enums::AttemptStatus, + Option, + types::PaymentsResponseData, + ), + errors::ConnectorError, +> { + let redirection_data_action = response.merchant_action.map(|merchant_action| { + ( + services::RedirectForm::from((merchant_action.data.redirect_url, Method::Get)), + merchant_action.action, + ) + }); + let (redirection_data, action) = match redirection_data_action { + Some((redirect_form, action)) => (Some(redirect_form), Some(action)), + None => (None, None), + }; + let status = enums::AttemptStatus::foreign_try_from((response.status, action))?; + let error = if utils::is_payment_failure(status) { + Some(types::ErrorResponse { + code: response + .reject_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .reject_reason + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.reject_reason, + status_code, + attempt_status: Some(status), + connector_transaction_id: Some(response.id.clone()), + }) + } else { + None + }; + let payment_response_data = types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(response.id.clone()), + redirection_data, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }; + Ok((status, error, payment_response_data)) +} + impl TryFrom> for types::RouterData { @@ -919,27 +975,12 @@ impl TryFrom, ) -> Result { - let redirection_data_action = value.response.merchant_action.map(|merchant_action| { - ( - services::RedirectForm::from((merchant_action.data.redirect_url, Method::Get)), - merchant_action.action, - ) - }); - let (redirection_data, action) = match redirection_data_action { - Some((redirect_form, action)) => (Some(redirect_form), Some(action)), - None => (None, None), - }; + let (status, error, payment_response_data) = + get_zen_response(value.response.clone(), value.http_code)?; Ok(Self { - status: enums::AttemptStatus::foreign_try_from((value.response.status, action))?, - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(value.response.id), - redirection_data, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - }), + status, + response: error.map_or_else(|| Ok(payment_response_data), Err), ..value.data }) } @@ -965,6 +1006,7 @@ impl TryFrom for enums::RefundStatus { } #[derive(Default, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct RefundResponse { id: String, status: RefundStatus, + reject_code: Option, + reject_reason: Option, } impl TryFrom> @@ -1025,17 +1070,44 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - let refund_status = enums::RefundStatus::from(item.response.status); + let (error, refund_response_data) = get_zen_refund_response(item.response, item.http_code)?; Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id, - refund_status, - }), + response: error.map_or_else(|| Ok(refund_response_data), Err), ..item.data }) } } +fn get_zen_refund_response( + response: RefundResponse, + status_code: u16, +) -> CustomResult<(Option, types::RefundsResponseData), errors::ConnectorError> +{ + let refund_status = enums::RefundStatus::from(response.status); + let error = if utils::is_refund_failure(refund_status) { + Some(types::ErrorResponse { + code: response + .reject_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .reject_reason + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.reject_reason, + status_code, + attempt_status: None, + connector_transaction_id: Some(response.id.clone()), + }) + } else { + None + }; + let refund_response_data = types::RefundsResponseData { + connector_refund_id: response.id, + refund_status, + }; + Ok((error, refund_response_data)) +} + impl TryFrom> for types::RefundsRouterData { diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 410e3c1113b1..3c3f01dc5f97 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -1,5 +1,6 @@ #[cfg(feature = "olap")] pub mod user; +pub mod user_role; // ID generation pub(crate) const ID_LENGTH: usize = 20; @@ -23,11 +24,20 @@ pub const REQUEST_TIMEOUT_ERROR_MESSAGE_FROM_PSYNC: &str = ///Payment intent fulfillment default timeout (in seconds) pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; +/// Payment intent default client secret expiry (in seconds) +pub const DEFAULT_SESSION_EXPIRY: i64 = 15 * 60; + +/// The length of a merchant fingerprint secret +pub const FINGERPRINT_SECRET_LENGTH: usize = 64; + // String literals pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; pub(crate) const NO_ERROR_CODE: &str = "No error code"; pub(crate) const UNSUPPORTED_ERROR_MESSAGE: &str = "Unsupported response type"; +pub(crate) const LOW_BALANCE_ERROR_MESSAGE: &str = "Insufficient balance in the payment method"; pub(crate) const CONNECTOR_UNAUTHORIZED_ERROR: &str = "Authentication Error from the connector"; +pub(crate) const CANNOT_CONTINUE_AUTH: &str = + "Cannot continue with Authorization due to failed Liability Shift."; // General purpose base64 engines pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose = @@ -56,5 +66,31 @@ pub const ROUTING_CONFIG_ID_LENGTH: usize = 10; pub const LOCKER_REDIS_PREFIX: &str = "LOCKER_PM_TOKEN"; pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes -#[cfg(any(feature = "olap", feature = "oltp"))] pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days + +pub const USER_BLACKLIST_PREFIX: &str = "BU_"; + +#[cfg(feature = "email")] +pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day + +#[cfg(feature = "email")] +pub const EMAIL_TOKEN_BLACKLIST_PREFIX: &str = "BET_"; + +#[cfg(feature = "olap")] +pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; +#[cfg(feature = "olap")] +pub const VERIFY_CONNECTOR_MERCHANT_ID: &str = "test_merchant"; + +#[cfg(feature = "olap")] +pub const CONNECTOR_ONBOARDING_CONFIG_PREFIX: &str = "onboarding"; + +/// Max payment session expiry +pub const MAX_SESSION_EXPIRY: u32 = 7890000; + +/// Min payment session expiry +pub const MIN_SESSION_EXPIRY: u32 = 60; + +pub const LOCKER_HEALTH_CALL_PATH: &str = "/health"; + +// URL for checking the outgoing call +pub const OUTGOING_CALL_URL: &str = "https://api.stripe.com/healthcheck"; diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs index 3a71fed01a12..1cda969f780e 100644 --- a/crates/router/src/consts/user.rs +++ b/crates/router/src/consts/user.rs @@ -1,8 +1,3 @@ -#[cfg(feature = "olap")] pub const MAX_NAME_LENGTH: usize = 70; -#[cfg(feature = "olap")] pub const MAX_COMPANY_NAME_LENGTH: usize = 70; - -// USER ROLES -#[cfg(any(feature = "olap", feature = "oltp"))] -pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; +pub const BUSINESS_EMAIL: &str = "biz@hyperswitch.io"; diff --git a/crates/router/src/consts/user_role.rs b/crates/router/src/consts/user_role.rs new file mode 100644 index 000000000000..ae1436bcd678 --- /dev/null +++ b/crates/router/src/consts/user_role.rs @@ -0,0 +1,11 @@ +// User Roles +pub const ROLE_ID_INTERNAL_VIEW_ONLY_USER: &str = "internal_view_only"; +pub const ROLE_ID_INTERNAL_ADMIN: &str = "internal_admin"; +pub const ROLE_ID_MERCHANT_ADMIN: &str = "merchant_admin"; +pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; +pub const ROLE_ID_MERCHANT_VIEW_ONLY: &str = "merchant_view_only"; +pub const ROLE_ID_MERCHANT_IAM_ADMIN: &str = "merchant_iam_admin"; +pub const ROLE_ID_MERCHANT_DEVELOPER: &str = "merchant_developer"; +pub const ROLE_ID_MERCHANT_OPERATOR: &str = "merchant_operator"; +pub const ROLE_ID_MERCHANT_CUSTOMER_SUPPORT: &str = "merchant_customer_support"; +pub const INTERNAL_USER_MERCHANT_ID: &str = "juspay000"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 8cc85eef60d6..9bdc493e0786 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -1,14 +1,23 @@ pub mod admin; pub mod api_keys; pub mod api_locking; +pub mod blocklist; pub mod cache; pub mod cards_info; +pub mod conditional_config; pub mod configs; +#[cfg(feature = "olap")] +pub mod connector_onboarding; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub mod currency; pub mod customers; pub mod disputes; pub mod errors; pub mod files; +#[cfg(feature = "frm")] +pub mod fraud_check; pub mod gsm; +pub mod health_check; pub mod locker_migration; pub mod mandate; pub mod metrics; @@ -17,11 +26,17 @@ pub mod payment_methods; pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; +pub mod pm_auth; pub mod refunds; pub mod routing; +pub mod surcharge_decision_config; #[cfg(feature = "olap")] pub mod user; +#[cfg(feature = "olap")] +pub mod user_role; pub mod utils; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; +#[cfg(feature = "olap")] +pub mod verify_connector; pub mod webhooks; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 39b4749535b7..364c5b9b2124 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -10,9 +10,11 @@ use common_utils::{ ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, pii, }; -use error_stack::{report, FutureExt, ResultExt}; +use diesel_models::configs; +use error_stack::{report, FutureExt, IntoReport, ResultExt}; use futures::future::try_join_all; use masking::{PeekInterface, Secret}; +use pm_auth::connector::plaid::transformers::PlaidAuthType; use uuid::Uuid; use crate::{ @@ -140,16 +142,16 @@ pub async fn create_merchant_account( .transpose()? .map(Secret::new); - let payment_link_config = req - .payment_link_config - .as_ref() - .map(|pl_metadata| { - utils::Encode::::encode_to_value(pl_metadata) - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_link_config", - }) + let fingerprint = Some(utils::generate_id(consts::FINGERPRINT_SECRET_LENGTH, "fs")); + if let Some(fingerprint) = fingerprint { + db.insert_config(configs::ConfigNew { + key: format!("fingerprint_secret_{}", req.merchant_id), + config: fingerprint, }) - .transpose()?; + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Mot able to generate Merchant fingerprint")?; + }; let organization_id = if let Some(organization_id) = req.organization_id.as_ref() { db.find_organization_by_org_id(organization_id) @@ -199,15 +201,15 @@ pub async fn create_merchant_account( primary_business_details, created_at: date_time::now(), modified_at: date_time::now(), + intent_fulfillment_time: None, frm_routing_algorithm: req.frm_routing_algorithm, - intent_fulfillment_time: req.intent_fulfillment_time.map(i64::from), payout_routing_algorithm: req.payout_routing_algorithm, id: None, organization_id, is_recon_enabled: false, default_profile: None, recon_status: diesel_models::enums::ReconStatus::NotRequested, - payment_link_config, + payment_link_config: None, }) } .await @@ -429,6 +431,8 @@ pub async fn update_business_profile_cascade( frm_routing_algorithm: None, payout_routing_algorithm: None, applepay_verified_domains: None, + payment_link_config: None, + session_expiry: None, }; let update_futures = business_profiles.iter().map(|business_profile| async { @@ -581,10 +585,10 @@ pub async fn merchant_account_update( publishable_key: None, primary_business_details, frm_routing_algorithm: req.frm_routing_algorithm, - intent_fulfillment_time: req.intent_fulfillment_time.map(i64::from), + intent_fulfillment_time: None, payout_routing_algorithm: req.payout_routing_algorithm, default_profile: business_profile_id_update, - payment_link_config: req.payment_link_config, + payment_link_config: None, }; let response = db @@ -762,7 +766,7 @@ pub async fn create_payment_connector( ) .await?; - let routable_connector = + let mut routable_connector = api_enums::RoutableConnectors::from_str(&req.connector_name.to_string()).ok(); let business_profile = state @@ -773,6 +777,28 @@ pub async fn create_payment_connector( id: profile_id.to_owned(), })?; + let pm_auth_connector = + api_enums::convert_pm_auth_connector(req.connector_name.to_string().as_str()); + + if pm_auth_connector.is_some() { + if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid connector type given".to_string(), + }) + .into_report(); + } + } else { + let routable_connector_option = req + .connector_name + .to_string() + .parse() + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid connector name given".to_string(), + })?; + routable_connector = Some(routable_connector_option); + }; + // If connector label is not passed in the request, generate one let connector_label = req .connector_label @@ -839,35 +865,40 @@ pub async fn create_payment_connector( // The purpose of this merchant account update is just to update the // merchant account `modified_at` field for KGraph cache invalidation - let merchant_account_update = storage::MerchantAccountUpdate::Update { - merchant_name: None, - merchant_details: None, - return_url: None, - webhook_details: None, - sub_merchants_enabled: None, - parent_merchant_id: None, - enable_payment_response_hash: None, - locker_id: None, - payment_response_hash_key: None, - primary_business_details: None, - metadata: None, - publishable_key: None, - redirect_to_merchant_with_http_post: None, - routing_algorithm: None, - intent_fulfillment_time: None, - frm_routing_algorithm: None, - payout_routing_algorithm: None, - default_profile: None, - payment_link_config: None, - }; - state .store - .update_specific_fields_in_merchant(merchant_id, merchant_account_update, &key_store) + .update_specific_fields_in_merchant( + merchant_id, + storage::MerchantAccountUpdate::ModifiedAtUpdate, + &key_store, + ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error updating the merchant account when creating payment connector")?; + let (connector_status, disabled) = validate_status_and_disabled( + req.status, + req.disabled, + auth, + // The validate_status_and_disabled function will use this value only + // when the status can be active. So we are passing this as fallback. + api_enums::ConnectorStatus::Active, + )?; + + if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { + if let Some(val) = req.pm_auth_config.clone() { + validate_pm_auth( + val, + &*state.clone().store, + merchant_id.clone().as_str(), + &key_store, + merchant_account, + &Some(profile_id.clone()), + ) + .await?; + } + } + let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, @@ -886,7 +917,7 @@ pub async fn create_payment_connector( .attach_printable("Unable to encrypt connector account details")?, payment_methods_enabled, test_mode: req.test_mode, - disabled: req.disabled, + disabled, metadata: req.metadata, frm_configs, connector_label: Some(connector_label), @@ -911,6 +942,7 @@ pub async fn create_payment_connector( profile_id: Some(profile_id.clone()), applepay_verified_domains: None, pm_auth_config: req.pm_auth_config.clone(), + status: connector_status, }; let mut default_routing_config = @@ -938,7 +970,7 @@ pub async fn create_payment_connector( #[cfg(feature = "connector_choice_mca_id")] merchant_connector_id: Some(mca.merchant_connector_id.clone()), #[cfg(not(feature = "connector_choice_mca_id"))] - sub_label: req.business_sub_label, + sub_label: req.business_sub_label.clone(), }; if !default_routing_config.contains(&choice) { @@ -946,7 +978,7 @@ pub async fn create_payment_connector( routing_helpers::update_merchant_default_config( &*state.store, merchant_id, - default_routing_config, + default_routing_config.clone(), ) .await?; } @@ -955,7 +987,7 @@ pub async fn create_payment_connector( routing_helpers::update_merchant_default_config( &*state.store, &profile_id.clone(), - default_routing_config_for_profile, + default_routing_config_for_profile.clone(), ) .await?; } @@ -974,6 +1006,54 @@ pub async fn create_payment_connector( Ok(service_api::ApplicationResponse::Json(mca_response)) } +async fn validate_pm_auth( + val: serde_json::Value, + db: &dyn StorageInterface, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, + profile_id: &Option, +) -> RouterResponse<()> { + let config = serde_json::from_value::(val) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "invalid data received for payment method auth config".to_string(), + }) + .attach_printable("Failed to deserialize Payment Method Auth config")?; + + let all_mcas = db + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + merchant_id, + true, + key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + })?; + + for conn_choice in config.enabled_payment_methods { + let pm_auth_mca = all_mcas + .clone() + .into_iter() + .find(|mca| mca.merchant_connector_id == conn_choice.mca_id) + .ok_or(errors::ApiErrorResponse::GenericNotFoundError { + message: "payment method auth connector account not found".to_string(), + }) + .into_report()?; + + if &pm_auth_mca.profile_id != profile_id { + return Err(errors::ApiErrorResponse::GenericNotFoundError { + message: "payment method auth profile_id differs from connector profile_id" + .to_string(), + }) + .into_report(); + } + } + + Ok(services::ApplicationResponse::StatusOk) +} + pub async fn retrieve_payment_connector( state: AppState, merchant_id: String, @@ -1056,7 +1136,7 @@ pub async fn update_payment_connector( .await .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; - let _merchant_account = db + let merchant_account = db .find_merchant_account_by_merchant_id(merchant_id, &key_store) .await .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; @@ -1083,6 +1163,72 @@ pub async fn update_payment_connector( let frm_configs = get_frm_config_as_secret(req.frm_configs); + let auth: types::ConnectorAuthType = req + .connector_account_details + .clone() + .unwrap_or(mca.connector_account_details.clone().into_inner()) + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "connector_account_details".to_string(), + expected_format: "auth_type and api_key".to_string(), + })?; + let connector_name = mca.connector_name.as_ref(); + let connector_enum = api_models::enums::Connector::from_str(connector_name) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", + }) + .attach_printable_lazy(|| format!("unable to parse connector name {connector_name:?}"))?; + validate_auth_and_metadata_type(connector_enum, &auth, &req.metadata).map_err( + |err| match *err.current_context() { + errors::ConnectorError::InvalidConnectorName => { + err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "The connector name is invalid".to_string(), + }) + } + errors::ConnectorError::InvalidConnectorConfig { config: field_name } => err + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: format!("The {} is invalid", field_name), + }), + errors::ConnectorError::FailedToObtainAuthType => { + err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "The auth type is invalid for the connector".to_string(), + }) + } + _ => err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "The request body is invalid".to_string(), + }), + }, + )?; + + let (connector_status, disabled) = + validate_status_and_disabled(req.status, req.disabled, auth, mca.status)?; + + if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { + if let Some(val) = req.pm_auth_config.clone() { + validate_pm_auth( + val, + db, + merchant_id, + &key_store, + merchant_account, + &mca.profile_id, + ) + .await?; + } + } + + // The purpose of this merchant account update is just to update the + // merchant account `modified_at` field for KGraph cache invalidation + db.update_specific_fields_in_merchant( + merchant_id, + storage::MerchantAccountUpdate::ModifiedAtUpdate, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error updating the merchant account when updating payment connector")?; + let payment_connector = storage::MerchantConnectorAccountUpdate::Update { merchant_id: None, connector_type: Some(req.connector_type), @@ -1098,7 +1244,7 @@ pub async fn update_payment_connector( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while encrypting data")?, test_mode: req.test_mode, - disabled: req.disabled, + disabled, payment_methods_enabled, metadata: req.metadata, frm_configs, @@ -1115,6 +1261,7 @@ pub async fn update_payment_connector( }, applepay_verified_domains: None, pm_auth_config: req.pm_auth_config, + status: Some(connector_status), }; let updated_mca = db @@ -1311,6 +1458,9 @@ pub async fn create_business_profile( request: api::BusinessProfileCreate, merchant_id: &str, ) -> RouterResponse { + if let Some(session_expiry) = &request.session_expiry { + helpers::validate_session_expiry(session_expiry.to_owned())?; + } let db = state.store.as_ref(); let key_store = db .get_merchant_key_store_by_merchant_id(merchant_id, &db.get_master_key().to_vec().into()) @@ -1424,6 +1574,10 @@ pub async fn update_business_profile( })? } + if let Some(session_expiry) = &request.session_expiry { + helpers::validate_session_expiry(session_expiry.to_owned())?; + } + let webhook_details = request .webhook_details .as_ref() @@ -1446,6 +1600,17 @@ pub async fn update_business_profile( .attach_printable("Invalid routing algorithm given")?; } + let payment_link_config = request + .payment_link_config + .as_ref() + .map(|pl_metadata| { + utils::Encode::::encode_to_value(pl_metadata) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", + }) + }) + .transpose()?; + let business_profile_update = storage::business_profile::BusinessProfileUpdateInternal { profile_name: request.profile_name, modified_at: Some(date_time::now()), @@ -1461,6 +1626,8 @@ pub async fn update_business_profile( payout_routing_algorithm: request.payout_routing_algorithm, is_recon_enabled: None, applepay_verified_domains: request.applepay_verified_domains, + payment_link_config, + session_expiry: request.session_expiry.map(i64::from), }; let updated_business_profile = db @@ -1546,9 +1713,9 @@ pub(crate) fn validate_auth_and_metadata_type( checkout::transformers::CheckoutAuthType::try_from(val)?; Ok(()) } - api_enums::Connector::Coinbase => { coinbase::transformers::CoinbaseAuthType::try_from(val)?; + coinbase::transformers::CoinbaseConnectorMeta::try_from(connector_meta_data)?; Ok(()) } api_enums::Connector::Cryptopay => { @@ -1565,6 +1732,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 => { @@ -1635,6 +1803,10 @@ pub(crate) fn validate_auth_and_metadata_type( payu::transformers::PayuAuthType::try_from(val)?; Ok(()) } + api_enums::Connector::Placetopay => { + placetopay::transformers::PlacetopayAuthType::try_from(val)?; + Ok(()) + } api_enums::Connector::Powertranz => { powertranz::transformers::PowertranzAuthType::try_from(val)?; Ok(()) @@ -1691,9 +1863,17 @@ pub(crate) fn validate_auth_and_metadata_type( zen::transformers::ZenAuthType::try_from(val)?; Ok(()) } - api_enums::Connector::Signifyd | api_enums::Connector::Plaid => { - Err(report!(errors::ConnectorError::InvalidConnectorName) - .attach_printable(format!("invalid connector name: {connector_name}"))) + api_enums::Connector::Signifyd => { + signifyd::transformers::SignifydAuthType::try_from(val)?; + Ok(()) + } + api_enums::Connector::Riskified => { + riskified::transformers::RiskifiedAuthType::try_from(val)?; + Ok(()) + } + api_enums::Connector::Plaid => { + PlaidAuthType::foreign_try_from(val)?; + Ok(()) } } } @@ -1722,3 +1902,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/api_keys.rs b/crates/router/src/core/api_keys.rs index c1ddc43cd65d..f28d845609a1 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -2,8 +2,12 @@ use common_utils::date_time; #[cfg(feature = "email")] use diesel_models::{api_keys::ApiKey, enums as storage_enums}; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms; +#[cfg(not(feature = "kms"))] +use masking::ExposeInterface; use masking::{PeekInterface, StrongSecret}; use router_env::{instrument, tracing}; @@ -35,19 +39,37 @@ static HASH_KEY: tokio::sync::OnceCell errors::RouterResult<&'static StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> { HASH_KEY .get_or_try_init(|| async { + let hash_key = { + #[cfg(feature = "kms")] + { + api_key_config.kms_encrypted_hash_key.clone() + } + #[cfg(not(feature = "kms"))] + { + masking::Secret::<_, masking::WithType>::new(api_key_config.hash_key.clone()) + } + }; + + #[cfg(feature = "hashicorp-vault")] + let hash_key = hash_key + .fetch_inner::(hc_client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + #[cfg(feature = "kms")] - let hash_key = api_key_config - .kms_encrypted_hash_key + let hash_key = hash_key .decrypt_inner(kms_client) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to KMS decrypt API key hashing key")?; #[cfg(not(feature = "kms"))] - let hash_key = &api_key_config.hash_key; + let hash_key = hash_key.expose(); <[u8; PlaintextApiKey::HASH_KEY_LEN]>::try_from( hex::decode(hash_key) @@ -132,6 +154,8 @@ impl PlaintextApiKey { pub async fn create_api_key( state: AppState, #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] + hc_client: &external_services::hashicorp_vault::HashiCorpVault, api_key: api::CreateApiKeyRequest, merchant_id: String, ) -> RouterResponse { @@ -153,6 +177,8 @@ pub async fn create_api_key( api_key_config, #[cfg(feature = "kms")] kms_client, + #[cfg(feature = "hashicorp-vault")] + hc_client, ) .await?; let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH); @@ -270,6 +296,11 @@ pub async fn add_api_key_expiry_task( api_key_expiry_tracker.key_id ) })?; + metrics::TASKS_ADDED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes("flow", "ApiKeyExpiry")], + ); Ok(()) } @@ -560,6 +591,10 @@ mod tests { &settings.api_keys, #[cfg(feature = "kms")] external_services::kms::get_kms_client(&settings.kms).await, + #[cfg(feature = "hashicorp-vault")] + external_services::hashicorp_vault::get_hashicorp_client(&settings.hc_vault) + .await + .unwrap(), ) .await .unwrap(); diff --git a/crates/router/src/core/blocklist.rs b/crates/router/src/core/blocklist.rs new file mode 100644 index 000000000000..85845602449c --- /dev/null +++ b/crates/router/src/core/blocklist.rs @@ -0,0 +1,41 @@ +pub mod transformers; +pub mod utils; + +use api_models::blocklist as api_blocklist; + +use crate::{ + core::errors::{self, RouterResponse}, + routes::AppState, + services, + types::domain, +}; + +pub async fn add_entry_to_blocklist( + state: AppState, + merchant_account: domain::MerchantAccount, + body: api_blocklist::AddToBlocklistRequest, +) -> RouterResponse { + utils::insert_entry_into_blocklist(&state, merchant_account.merchant_id, body) + .await + .map(services::ApplicationResponse::Json) +} + +pub async fn remove_entry_from_blocklist( + state: AppState, + merchant_account: domain::MerchantAccount, + body: api_blocklist::DeleteFromBlocklistRequest, +) -> RouterResponse { + utils::delete_entry_from_blocklist(&state, merchant_account.merchant_id, body) + .await + .map(services::ApplicationResponse::Json) +} + +pub async fn list_blocklist_entries( + state: AppState, + merchant_account: domain::MerchantAccount, + query: api_blocklist::ListBlocklistQuery, +) -> RouterResponse> { + utils::list_blocklist_entries_for_merchant(&state, merchant_account.merchant_id, query) + .await + .map(services::ApplicationResponse::Json) +} diff --git a/crates/router/src/core/blocklist/transformers.rs b/crates/router/src/core/blocklist/transformers.rs new file mode 100644 index 000000000000..2cb5f86a264a --- /dev/null +++ b/crates/router/src/core/blocklist/transformers.rs @@ -0,0 +1,13 @@ +use api_models::blocklist; + +use crate::types::{storage, transformers::ForeignFrom}; + +impl ForeignFrom for blocklist::AddToBlocklistResponse { + fn foreign_from(from: storage::Blocklist) -> Self { + Self { + fingerprint_id: from.fingerprint_id, + data_kind: from.data_kind, + created_at: from.created_at, + } + } +} diff --git a/crates/router/src/core/blocklist/utils.rs b/crates/router/src/core/blocklist/utils.rs new file mode 100644 index 000000000000..b7effaf63acf --- /dev/null +++ b/crates/router/src/core/blocklist/utils.rs @@ -0,0 +1,359 @@ +use api_models::blocklist as api_blocklist; +use common_utils::crypto::{self, SignMessage}; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::kms; + +use super::{errors, AppState}; +use crate::{ + consts, + core::errors::{RouterResult, StorageErrorExt}, + types::{storage, transformers::ForeignInto}, + utils, +}; + +pub async fn delete_entry_from_blocklist( + state: &AppState, + merchant_id: String, + request: api_blocklist::DeleteFromBlocklistRequest, +) -> RouterResult { + let blocklist_entry = match request { + api_blocklist::DeleteFromBlocklistRequest::CardBin(bin) => { + delete_card_bin_blocklist_entry(state, &bin, &merchant_id).await? + } + + api_blocklist::DeleteFromBlocklistRequest::ExtendedCardBin(xbin) => { + delete_card_bin_blocklist_entry(state, &xbin, &merchant_id).await? + } + + api_blocklist::DeleteFromBlocklistRequest::Fingerprint(fingerprint_id) => { + let blocklist_fingerprint = state + .store + .find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &merchant_id, + &fingerprint_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "blocklist record with given fingerprint id not found".to_string(), + })?; + + #[cfg(feature = "kms")] + let decrypted_fingerprint = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(blocklist_fingerprint.encrypted_fingerprint) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to kms decrypt fingerprint")?; + + #[cfg(not(feature = "kms"))] + let decrypted_fingerprint = blocklist_fingerprint.encrypted_fingerprint; + + let blocklist_entry = state + .store + .delete_blocklist_entry_by_merchant_id_fingerprint_id(&merchant_id, &fingerprint_id) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "no blocklist record for the given fingerprint id was found" + .to_string(), + })?; + + state + .store + .delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &merchant_id, + &decrypted_fingerprint, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "no blocklist record for the given fingerprint id was found" + .to_string(), + })?; + + blocklist_entry + } + }; + + Ok(blocklist_entry.foreign_into()) +} + +pub async fn list_blocklist_entries_for_merchant( + state: &AppState, + merchant_id: String, + query: api_blocklist::ListBlocklistQuery, +) -> RouterResult> { + state + .store + .list_blocklist_entries_by_merchant_id_data_kind( + &merchant_id, + query.data_kind, + query.limit.into(), + query.offset.into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "no blocklist records found".to_string(), + }) + .map(|v| v.into_iter().map(ForeignInto::foreign_into).collect()) +} + +fn validate_card_bin(bin: &str) -> RouterResult<()> { + if bin.len() == 6 && bin.chars().all(|c| c.is_ascii_digit()) { + Ok(()) + } else { + Err(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "data".to_string(), + expected_format: "a 6 digit number".to_string(), + }) + .into_report() + } +} + +fn validate_extended_card_bin(bin: &str) -> RouterResult<()> { + if bin.len() == 8 && bin.chars().all(|c| c.is_ascii_digit()) { + Ok(()) + } else { + Err(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "data".to_string(), + expected_format: "an 8 digit number".to_string(), + }) + .into_report() + } +} + +pub async fn insert_entry_into_blocklist( + state: &AppState, + merchant_id: String, + to_block: api_blocklist::AddToBlocklistRequest, +) -> RouterResult { + let blocklist_entry = match &to_block { + api_blocklist::AddToBlocklistRequest::CardBin(bin) => { + validate_card_bin(bin)?; + duplicate_check_insert_bin( + bin, + state, + &merchant_id, + common_enums::BlocklistDataKind::CardBin, + ) + .await? + } + + api_blocklist::AddToBlocklistRequest::ExtendedCardBin(bin) => { + validate_extended_card_bin(bin)?; + duplicate_check_insert_bin( + bin, + state, + &merchant_id, + common_enums::BlocklistDataKind::ExtendedCardBin, + ) + .await? + } + + api_blocklist::AddToBlocklistRequest::Fingerprint(fingerprint_id) => { + let blocklist_entry_result = state + .store + .find_blocklist_entry_by_merchant_id_fingerprint_id(&merchant_id, fingerprint_id) + .await; + + match blocklist_entry_result { + Ok(_) => { + return Err(errors::ApiErrorResponse::PreconditionFailed { + message: "data associated with the given fingerprint is already blocked" + .to_string(), + }) + .into_report(); + } + + // if it is a db not found error, we can proceed as normal + Err(inner) if inner.current_context().is_db_not_found() => {} + + err @ Err(_) => { + err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error fetching blocklist entry from table")?; + } + } + + let blocklist_fingerprint = state + .store + .find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &merchant_id, + fingerprint_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "fingerprint not found".to_string(), + })?; + + #[cfg(feature = "kms")] + let decrypted_fingerprint = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(blocklist_fingerprint.encrypted_fingerprint) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to kms decrypt encrypted fingerprint")?; + + #[cfg(not(feature = "kms"))] + let decrypted_fingerprint = blocklist_fingerprint.encrypted_fingerprint; + + state + .store + .insert_blocklist_lookup_entry( + diesel_models::blocklist_lookup::BlocklistLookupNew { + merchant_id: merchant_id.clone(), + fingerprint: decrypted_fingerprint, + }, + ) + .await + .to_duplicate_response(errors::ApiErrorResponse::PreconditionFailed { + message: "the payment instrument associated with the given fingerprint is already in the blocklist".to_string(), + }) + .attach_printable("failed to add fingerprint to blocklist lookup")?; + + state + .store + .insert_blocklist_entry(storage::BlocklistNew { + merchant_id: merchant_id.clone(), + fingerprint_id: fingerprint_id.clone(), + data_kind: blocklist_fingerprint.data_kind, + metadata: None, + created_at: common_utils::date_time::now(), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to add fingerprint to pm blocklist")? + } + }; + + Ok(blocklist_entry.foreign_into()) +} + +pub async fn get_merchant_fingerprint_secret( + state: &AppState, + merchant_id: &str, +) -> RouterResult { + let key = get_merchant_fingerprint_secret_key(merchant_id); + let config_fetch_result = state.store.find_config_by_key(&key).await; + + match config_fetch_result { + Ok(config) => Ok(config.config), + + Err(e) if e.current_context().is_db_not_found() => { + let new_fingerprint_secret = + utils::generate_id(consts::FINGERPRINT_SECRET_LENGTH, "fs"); + let new_config = storage::ConfigNew { + key, + config: new_fingerprint_secret.clone(), + }; + + state + .store + .insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to create new fingerprint secret for merchant")?; + + Ok(new_fingerprint_secret) + } + + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error fetching merchant fingerprint secret"), + } +} + +pub fn get_merchant_fingerprint_secret_key(merchant_id: &str) -> String { + format!("fingerprint_secret_{merchant_id}") +} + +async fn duplicate_check_insert_bin( + bin: &str, + state: &AppState, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, +) -> RouterResult { + let merchant_secret = get_merchant_fingerprint_secret(state, merchant_id).await?; + let bin_fingerprint = crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_secret.clone().as_bytes(), + bin.as_bytes(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error in bin hash creation")?; + + let encoded_fingerprint = hex::encode(bin_fingerprint.clone()); + + let blocklist_entry_result = state + .store + .find_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, bin) + .await; + + match blocklist_entry_result { + Ok(_) => { + return Err(errors::ApiErrorResponse::PreconditionFailed { + message: "provided bin is already blocked".to_string(), + }) + .into_report(); + } + + Err(e) if e.current_context().is_db_not_found() => {} + + err @ Err(_) => { + return err + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to fetch blocklist entry"); + } + } + + // Checking for duplicacy + state + .store + .insert_blocklist_lookup_entry(diesel_models::blocklist_lookup::BlocklistLookupNew { + merchant_id: merchant_id.to_string(), + fingerprint: encoded_fingerprint.clone(), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error inserting blocklist lookup entry")?; + + state + .store + .insert_blocklist_entry(storage::BlocklistNew { + merchant_id: merchant_id.to_string(), + fingerprint_id: bin.to_string(), + data_kind, + metadata: None, + created_at: common_utils::date_time::now(), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error inserting pm blocklist item") +} + +async fn delete_card_bin_blocklist_entry( + state: &AppState, + bin: &str, + merchant_id: &str, +) -> RouterResult { + let merchant_secret = get_merchant_fingerprint_secret(state, merchant_id).await?; + let bin_fingerprint = crypto::HmacSha512 + .sign_message(merchant_secret.as_bytes(), bin.as_bytes()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error when hashing card bin")?; + let encoded_fingerprint = hex::encode(bin_fingerprint); + + state + .store + .delete_blocklist_lookup_entry_by_merchant_id_fingerprint(merchant_id, &encoded_fingerprint) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "could not find a blocklist entry for the given bin".to_string(), + })?; + + state + .store + .delete_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, bin) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "could not find a blocklist entry for the given bin".to_string(), + }) +} 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/connector_onboarding.rs b/crates/router/src/core/connector_onboarding.rs new file mode 100644 index 000000000000..e6c1fc9d378d --- /dev/null +++ b/crates/router/src/core/connector_onboarding.rs @@ -0,0 +1,116 @@ +use api_models::{connector_onboarding as api, enums}; +use masking::Secret; + +use crate::{ + core::errors::{ApiErrorResponse, RouterResponse, RouterResult}, + services::{authentication as auth, ApplicationResponse}, + types::{self as oss_types}, + utils::connector_onboarding as utils, + AppState, +}; + +pub mod paypal; + +#[async_trait::async_trait] +pub trait AccessToken { + async fn access_token(state: &AppState) -> RouterResult; +} + +pub async fn get_action_url( + state: AppState, + user_from_token: auth::UserFromToken, + request: api::ActionUrlRequest, +) -> RouterResponse { + utils::check_if_connector_exists(&state, &request.connector_id, &user_from_token.merchant_id) + .await?; + + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); + let is_enabled = utils::is_enabled(request.connector, &connector_onboarding_conf); + let tracking_id = + utils::get_tracking_id_from_configs(&state, &request.connector_id, request.connector) + .await?; + + match (is_enabled, request.connector) { + (Some(true), enums::Connector::Paypal) => { + let action_url = Box::pin(paypal::get_action_url_from_paypal( + state, + tracking_id, + request.return_url, + )) + .await?; + Ok(ApplicationResponse::Json(api::ActionUrlResponse::PayPal( + api::PayPalActionUrlResponse { action_url }, + ))) + } + _ => Err(ApiErrorResponse::FlowNotSupported { + flow: "Connector onboarding".to_string(), + connector: request.connector.to_string(), + } + .into()), + } +} + +pub async fn sync_onboarding_status( + state: AppState, + user_from_token: auth::UserFromToken, + request: api::OnboardingSyncRequest, +) -> RouterResponse { + utils::check_if_connector_exists(&state, &request.connector_id, &user_from_token.merchant_id) + .await?; + + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); + let is_enabled = utils::is_enabled(request.connector, &connector_onboarding_conf); + let tracking_id = + utils::get_tracking_id_from_configs(&state, &request.connector_id, request.connector) + .await?; + + match (is_enabled, request.connector) { + (Some(true), enums::Connector::Paypal) => { + let status = Box::pin(paypal::sync_merchant_onboarding_status( + state.clone(), + tracking_id, + )) + .await?; + if let api::OnboardingStatus::PayPal(api::PayPalOnboardingStatus::Success( + ref paypal_onboarding_data, + )) = status + { + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); + let auth_details = oss_types::ConnectorAuthType::SignatureKey { + api_key: connector_onboarding_conf.paypal.client_secret, + key1: connector_onboarding_conf.paypal.client_id, + api_secret: Secret::new(paypal_onboarding_data.payer_id.clone()), + }; + let update_mca_data = paypal::update_mca( + &state, + user_from_token.merchant_id, + request.connector_id.to_owned(), + auth_details, + ) + .await?; + + return Ok(ApplicationResponse::Json(api::OnboardingStatus::PayPal( + api::PayPalOnboardingStatus::ConnectorIntegrated(update_mca_data), + ))); + } + Ok(ApplicationResponse::Json(status)) + } + _ => Err(ApiErrorResponse::FlowNotSupported { + flow: "Connector onboarding".to_string(), + connector: request.connector.to_string(), + } + .into()), + } +} + +pub async fn reset_tracking_id( + state: AppState, + user_from_token: auth::UserFromToken, + request: api::ResetTrackingIdRequest, +) -> RouterResponse<()> { + utils::check_if_connector_exists(&state, &request.connector_id, &user_from_token.merchant_id) + .await?; + utils::set_tracking_id_in_configs(&state, &request.connector_id, request.connector).await?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/core/connector_onboarding/paypal.rs b/crates/router/src/core/connector_onboarding/paypal.rs new file mode 100644 index 000000000000..f18681f8cfdb --- /dev/null +++ b/crates/router/src/core/connector_onboarding/paypal.rs @@ -0,0 +1,170 @@ +use api_models::{admin::MerchantConnectorUpdate, connector_onboarding as api}; +use common_utils::ext_traits::Encode; +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, PeekInterface, Secret}; + +use crate::{ + core::{ + admin, + errors::{ApiErrorResponse, RouterResult}, + }, + services::{send_request, ApplicationResponse, Request}, + types::{self as oss_types, api as oss_api_types, api::connector_onboarding as types}, + utils::connector_onboarding as utils, + AppState, +}; + +fn build_referral_url(state: AppState) -> String { + format!( + "{}v2/customer/partner-referrals", + state.conf.connectors.paypal.base_url + ) +} + +async fn build_referral_request( + state: AppState, + tracking_id: String, + return_url: String, +) -> RouterResult { + let access_token = utils::paypal::generate_access_token(state.clone()).await?; + let request_body = types::paypal::PartnerReferralRequest::new(tracking_id, return_url); + + utils::paypal::build_paypal_post_request( + build_referral_url(state), + request_body, + access_token.token.expose(), + ) +} + +pub async fn get_action_url_from_paypal( + state: AppState, + tracking_id: String, + return_url: String, +) -> RouterResult { + let referral_request = Box::pin(build_referral_request( + state.clone(), + tracking_id, + return_url, + )) + .await?; + let referral_response = send_request(&state, referral_request, None) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to send request to paypal referrals")?; + + let parsed_response: types::paypal::PartnerReferralResponse = referral_response + .json() + .await + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse paypal response")?; + + parsed_response.extract_action_url() +} + +fn merchant_onboarding_status_url(state: AppState, tracking_id: String) -> String { + let partner_id = state.conf.connector_onboarding.paypal.partner_id.to_owned(); + format!( + "{}v1/customer/partners/{}/merchant-integrations?tracking_id={}", + state.conf.connectors.paypal.base_url, + partner_id.expose(), + tracking_id + ) +} + +pub async fn sync_merchant_onboarding_status( + state: AppState, + tracking_id: String, +) -> RouterResult { + let access_token = utils::paypal::generate_access_token(state.clone()).await?; + + let Some(seller_status_response) = + find_paypal_merchant_by_tracking_id(state.clone(), tracking_id, &access_token).await? + else { + return Ok(api::OnboardingStatus::PayPal( + api::PayPalOnboardingStatus::AccountNotFound, + )); + }; + + let merchant_details_url = seller_status_response + .extract_merchant_details_url(&state.conf.connectors.paypal.base_url)?; + + let merchant_details_request = + utils::paypal::build_paypal_get_request(merchant_details_url, access_token.token.expose())?; + + let merchant_details_response = send_request(&state, merchant_details_request, None) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to send request to paypal merchant details")?; + + let parsed_response: types::paypal::SellerStatusDetailsResponse = merchant_details_response + .json() + .await + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse paypal merchant details response")?; + + let eligibity = parsed_response.get_eligibility_status().await?; + Ok(api::OnboardingStatus::PayPal(eligibity)) +} + +async fn find_paypal_merchant_by_tracking_id( + state: AppState, + tracking_id: String, + access_token: &oss_types::AccessToken, +) -> RouterResult> { + let seller_status_request = utils::paypal::build_paypal_get_request( + merchant_onboarding_status_url(state.clone(), tracking_id), + access_token.token.peek().to_string(), + )?; + let seller_status_response = send_request(&state, seller_status_request, None) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to send request to paypal onboarding status")?; + + if seller_status_response.status().is_success() { + return Ok(Some( + seller_status_response + .json() + .await + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse paypal onboarding status response")?, + )); + } + Ok(None) +} + +pub async fn update_mca( + state: &AppState, + merchant_id: String, + connector_id: String, + auth_details: oss_types::ConnectorAuthType, +) -> RouterResult { + let connector_auth_json = + Encode::::encode_to_value(&auth_details) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error while deserializing connector_account_details")?; + + let request = MerchantConnectorUpdate { + connector_type: common_enums::ConnectorType::PaymentProcessor, + connector_account_details: Some(Secret::new(connector_auth_json)), + disabled: Some(false), + status: Some(common_enums::ConnectorStatus::Active), + test_mode: None, + connector_label: None, + payment_methods_enabled: None, + metadata: None, + frm_configs: None, + connector_webhook_details: None, + pm_auth_config: None, + }; + let mca_response = + admin::update_payment_connector(state.clone(), &merchant_id, &connector_id, request) + .await?; + + match mca_response { + ApplicationResponse::Json(mca_data) => Ok(mca_data), + _ => Err(ApiErrorResponse::InternalServerError.into()), + } +} diff --git a/crates/router/src/core/currency.rs b/crates/router/src/core/currency.rs new file mode 100644 index 000000000000..41699df47a76 --- /dev/null +++ b/crates/router/src/core/currency.rs @@ -0,0 +1,55 @@ +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, + #[cfg(feature = "hashicorp-vault")] + &state.conf.hc_vault, + ) + .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, + #[cfg(feature = "hashicorp-vault")] + &state.conf.hc_vault, + )) + .await + .change_context(ApiErrorResponse::InternalServerError)?, + )) +} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 810c079987eb..9052893d4a96 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::*, @@ -186,6 +186,12 @@ pub enum ConnectorError { InvalidConnectorConfig { config: &'static str }, } +#[derive(Debug, thiserror::Error)] +pub enum HealthCheckOutGoing { + #[error("Outgoing call failed with error: {message}")] + OutGoingFailed { message: String }, +} + #[derive(Debug, thiserror::Error)] pub enum VaultError { #[error("Failed to save card in card vault")] @@ -226,7 +232,7 @@ pub enum KmsError { Utf8DecodingFailed, } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, serde::Serialize)] pub enum WebhooksFlowError { #[error("Merchant webhook config not found")] MerchantConfigNotFound, @@ -375,3 +381,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/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index d34cbf88aaae..e83483b08169 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -60,7 +60,7 @@ pub enum ApiErrorResponse { CustomerRedacted, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_12", message = "Reached maximum refund attempts")] MaximumRefundCount, - #[error(error_type = ErrorType::InvalidRequestError, code = "IR_13", message = "Refund amount exceeds the payment amount")] + #[error(error_type = ErrorType::InvalidRequestError, code = "IR_13", message = "The refund amount exceeds the amount captured")] RefundAmountExceedsPaymentAmount, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_14", message = "This Payment could not be {current_flow} because it has a {field_name} of {current_value}. The expected state is {states}")] PaymentUnexpectedState { @@ -186,6 +186,8 @@ pub enum ApiErrorResponse { PaymentNotSucceeded, #[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "The specified merchant connector account is disabled")] MerchantConnectorAccountDisabled, + #[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "The specified payment is blocked")] + PaymentBlocked, #[error(error_type= ErrorType::ObjectNotFound, code = "HE_04", message = "Successful payment not found for the given payment id")] SuccessfulPaymentNotFound, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "The connector provided in the request is incorrect or not available")] @@ -236,8 +238,15 @@ pub enum ApiErrorResponse { WebhookInvalidMerchantSecret, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_19", message = "{message}")] CurrencyNotSupported { message: String }, + #[error(error_type = ErrorType::ServerNotAvailable, code= "HE_00", message = "{component} health check is failing with error: {message}")] + HealthCheckError { + component: &'static str, + message: String, + }, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_24", message = "Merchant connector account is configured with invalid {config}")] InvalidConnectorConfiguration { config: String }, + #[error(error_type = ErrorType::ValidationError, code = "HE_01", message = "Failed to convert currency to minor unit")] + CurrencyConversionFailed, } impl PTError for ApiErrorResponse { diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index 17aa6f3a207a..0119335b7c45 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -65,7 +65,7 @@ impl ErrorSwitch for ApiErrorRespon } Self::MaximumRefundCount => AER::BadRequest(ApiError::new("IR", 12, "Reached maximum refund attempts", None)), Self::RefundAmountExceedsPaymentAmount => { - AER::BadRequest(ApiError::new("IR", 13, "Refund amount exceeds the payment amount", None)) + AER::BadRequest(ApiError::new("IR", 13, "The refund amount exceeds the amount captured", None)) } Self::PaymentUnexpectedState { current_flow, @@ -123,7 +123,10 @@ impl ErrorSwitch for ApiErrorRespon }, Self::MandateUpdateFailed | Self::MandateSerializationFailed | Self::MandateDeserializationFailed | Self::InternalServerError => { AER::InternalServerError(ApiError::new("HE", 0, "Something went wrong", None)) - } + }, + Self::HealthCheckError { message,component } => { + AER::InternalServerError(ApiError::new("HE",0,format!("{} health check failed with error: {}",component,message),None)) + }, Self::PayoutFailed { data } => { AER::BadRequest(ApiError::new("CE", 4, "Payout failed while processing with connector.", Some(Extra { data: data.clone(), ..Default::default()}))) }, @@ -187,6 +190,7 @@ impl ErrorSwitch for ApiErrorRespon AER::BadRequest(ApiError::new("HE", 3, "Mandate Validation Failed", Some(Extra { reason: Some(reason.clone()), ..Default::default() }))) } Self::PaymentNotSucceeded => AER::BadRequest(ApiError::new("HE", 3, "The payment has not succeeded yet. Please pass a successful payment to initiate refund", None)), + Self::PaymentBlocked => AER::BadRequest(ApiError::new("HE", 3, "The payment is blocked", None)), Self::SuccessfulPaymentNotFound => { AER::NotFound(ApiError::new("HE", 4, "Successful payment not found for the given payment id", None)) } @@ -270,6 +274,9 @@ impl ErrorSwitch for ApiErrorRespon Self::InvalidConnectorConfiguration {config} => { AER::BadRequest(ApiError::new("IR", 24, format!("Merchant connector account is configured with invalid {config}"), None)) } + Self::CurrencyConversionFailed => { + AER::Unprocessable(ApiError::new("HE", 2, "Failed to convert currency to minor unit", None)) + } } } } diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index b4d48365dc84..d3b1679378ea 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -4,6 +4,7 @@ use crate::services::ApplicationResponse; pub type UserResult = CustomResult; pub type UserResponse = CustomResult, UserErrors>; +pub mod sample_data; #[derive(Debug, thiserror::Error)] pub enum UserErrors { @@ -11,14 +12,24 @@ pub enum UserErrors { InternalServerError, #[error("InvalidCredentials")] InvalidCredentials, + #[error("UserNotFound")] + UserNotFound, #[error("UserExists")] UserExists, + #[error("LinkInvalid")] + LinkInvalid, + #[error("UnverifiedUser")] + UnverifiedUser, + #[error("InvalidOldPassword")] + InvalidOldPassword, #[error("EmailParsingError")] EmailParsingError, #[error("NameParsingError")] NameParsingError, #[error("PasswordParsingError")] PasswordParsingError, + #[error("UserAlreadyVerified")] + UserAlreadyVerified, #[error("CompanyNameParsingError")] CompanyNameParsingError, #[error("MerchantAccountCreationError: {0}")] @@ -27,6 +38,28 @@ 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, + #[error("ChangePasswordError")] + ChangePasswordError, + #[error("InvalidDeleteOperation")] + InvalidDeleteOperation, + #[error("MaxInvitationsError")] + MaxInvitationsError, + #[error("RoleNotFound")] + RoleNotFound, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -35,44 +68,122 @@ impl common_utils::errors::ErrorSwitch { - AER::InternalServerError(ApiError::new("HE", 0, "Something Went Wrong", None)) + AER::InternalServerError(ApiError::new("HE", 0, self.get_error_message(), None)) + } + Self::InvalidCredentials => { + AER::Unauthorized(ApiError::new(sub_code, 1, self.get_error_message(), None)) + } + Self::UserNotFound => { + AER::Unauthorized(ApiError::new(sub_code, 2, self.get_error_message(), None)) + } + Self::UserExists => { + AER::BadRequest(ApiError::new(sub_code, 3, self.get_error_message(), None)) + } + Self::LinkInvalid => { + AER::Unauthorized(ApiError::new(sub_code, 4, self.get_error_message(), None)) + } + Self::UnverifiedUser => { + AER::Unauthorized(ApiError::new(sub_code, 5, self.get_error_message(), None)) + } + Self::InvalidOldPassword => { + AER::BadRequest(ApiError::new(sub_code, 6, self.get_error_message(), None)) } - Self::InvalidCredentials => AER::Unauthorized(ApiError::new( - sub_code, - 1, - "Incorrect email or password", - None, - )), - Self::UserExists => AER::BadRequest(ApiError::new( - sub_code, - 3, - "An account already exists with this email", - None, - )), Self::EmailParsingError => { - AER::BadRequest(ApiError::new(sub_code, 7, "Invalid Email", None)) + AER::BadRequest(ApiError::new(sub_code, 7, self.get_error_message(), None)) } Self::NameParsingError => { - AER::BadRequest(ApiError::new(sub_code, 8, "Invalid Name", None)) + AER::BadRequest(ApiError::new(sub_code, 8, self.get_error_message(), None)) } Self::PasswordParsingError => { - AER::BadRequest(ApiError::new(sub_code, 9, "Invalid Password", None)) + AER::BadRequest(ApiError::new(sub_code, 9, self.get_error_message(), None)) + } + Self::UserAlreadyVerified => { + AER::Unauthorized(ApiError::new(sub_code, 11, self.get_error_message(), None)) } Self::CompanyNameParsingError => { - AER::BadRequest(ApiError::new(sub_code, 14, "Invalid Company Name", None)) + AER::BadRequest(ApiError::new(sub_code, 14, self.get_error_message(), None)) } Self::MerchantAccountCreationError(error_message) => { AER::InternalServerError(ApiError::new(sub_code, 15, error_message, None)) } Self::InvalidEmailError => { - AER::BadRequest(ApiError::new(sub_code, 16, "Invalid Email", None)) + AER::BadRequest(ApiError::new(sub_code, 16, self.get_error_message(), None)) + } + Self::MerchantIdNotFound => { + AER::BadRequest(ApiError::new(sub_code, 18, self.get_error_message(), None)) + } + Self::MetadataAlreadySet => { + AER::BadRequest(ApiError::new(sub_code, 19, self.get_error_message(), None)) } Self::DuplicateOrganizationId => AER::InternalServerError(ApiError::new( sub_code, 21, - "An Organization with the id already exists", + self.get_error_message(), None, )), + Self::InvalidRoleId => { + AER::BadRequest(ApiError::new(sub_code, 22, self.get_error_message(), None)) + } + Self::InvalidRoleOperation => { + AER::BadRequest(ApiError::new(sub_code, 23, self.get_error_message(), None)) + } + Self::IpAddressParsingFailed => AER::InternalServerError(ApiError::new( + sub_code, + 24, + self.get_error_message(), + None, + )), + Self::InvalidMetadataRequest => { + AER::BadRequest(ApiError::new(sub_code, 26, self.get_error_message(), None)) + } + Self::MerchantIdParsingError => { + AER::BadRequest(ApiError::new(sub_code, 28, self.get_error_message(), None)) + } + Self::ChangePasswordError => { + AER::BadRequest(ApiError::new(sub_code, 29, self.get_error_message(), None)) + } + Self::InvalidDeleteOperation => { + AER::BadRequest(ApiError::new(sub_code, 30, self.get_error_message(), None)) + } + Self::MaxInvitationsError => { + AER::BadRequest(ApiError::new(sub_code, 31, self.get_error_message(), None)) + } + Self::RoleNotFound => { + AER::BadRequest(ApiError::new(sub_code, 32, self.get_error_message(), None)) + } + } + } +} + +impl UserErrors { + pub fn get_error_message(&self) -> &str { + match self { + Self::InternalServerError => "Something went wrong", + Self::InvalidCredentials => "Incorrect email or password", + Self::UserNotFound => "Email doesn’t exist. Register", + Self::UserExists => "An account already exists with this email", + Self::LinkInvalid => "Invalid or expired link", + Self::UnverifiedUser => "Kindly verify your account", + Self::InvalidOldPassword => "Old password incorrect. Please enter the correct password", + Self::EmailParsingError => "Invalid Email", + Self::NameParsingError => "Invalid Name", + Self::PasswordParsingError => "Invalid Password", + Self::UserAlreadyVerified => "User already verified", + Self::CompanyNameParsingError => "Invalid Company Name", + Self::MerchantAccountCreationError(error_message) => error_message, + Self::InvalidEmailError => "Invalid Email", + Self::MerchantIdNotFound => "Invalid Merchant ID", + Self::MetadataAlreadySet => "Metadata already set", + Self::DuplicateOrganizationId => "An Organization with the id already exists", + Self::InvalidRoleId => "Invalid Role ID", + Self::InvalidRoleOperation => "User Role Operation Not Supported", + Self::IpAddressParsingFailed => "Something went wrong", + Self::InvalidMetadataRequest => "Invalid Metadata Request", + Self::MerchantIdParsingError => "Invalid Merchant Id", + Self::ChangePasswordError => "Old and new password cannot be the same", + Self::InvalidDeleteOperation => "Delete Operation Not Supported", + Self::MaxInvitationsError => "Maximum invite count per request exceeded", + Self::RoleNotFound => "Role Not Found", } } } diff --git a/crates/router/src/core/errors/user/sample_data.rs b/crates/router/src/core/errors/user/sample_data.rs new file mode 100644 index 000000000000..84c6c9fa43a2 --- /dev/null +++ b/crates/router/src/core/errors/user/sample_data.rs @@ -0,0 +1,57 @@ +use api_models::errors::types::{ApiError, ApiErrorResponse}; +use common_utils::errors::{CustomResult, ErrorSwitch, ErrorSwitchFrom}; +use data_models::errors::StorageError; + +pub type SampleDataResult = CustomResult; + +#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] +pub enum SampleDataError { + #[error["Internal Server Error"]] + InternalServerError, + #[error("Data Does Not Exist")] + DataDoesNotExist, + #[error("Invalid Parameters")] + InvalidParameters, + #[error["Invalid Records"]] + InvalidRange, +} + +impl ErrorSwitch for SampleDataError { + fn switch(&self) -> ApiErrorResponse { + match self { + Self::InternalServerError => ApiErrorResponse::InternalServerError(ApiError::new( + "SD", + 0, + "Something went wrong", + None, + )), + Self::DataDoesNotExist => ApiErrorResponse::NotFound(ApiError::new( + "SD", + 1, + "Sample Data not present for given request", + None, + )), + Self::InvalidParameters => ApiErrorResponse::BadRequest(ApiError::new( + "SD", + 2, + "Invalid parameters to generate Sample Data", + None, + )), + Self::InvalidRange => ApiErrorResponse::BadRequest(ApiError::new( + "SD", + 3, + "Records to be generated should be between range 10 and 100", + None, + )), + } + } +} + +impl ErrorSwitchFrom for SampleDataError { + fn switch_from(error: &StorageError) -> Self { + match matches!(error, StorageError::ValueNotFound(_)) { + true => Self::DataDoesNotExist, + false => Self::InternalServerError, + } + } +} diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index c3cdf95b87bd..a54dae1c0352 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,18 @@ 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::ConnectorError::NotImplemented(reason) => { + errors::ApiErrorResponse::NotImplemented { + message: errors::api_error_response::NotImplementedMessage::Reason( + reason.to_string(), + ), + } + } _ => errors::ApiErrorResponse::InternalServerError, }; err.change_context(error) @@ -419,3 +487,25 @@ impl ConnectorErrorExt for error_stack::Result } } } + +pub trait RedisErrorExt { + #[track_caller] + fn to_redis_failed_response(self, key: &str) -> error_stack::Report; +} + +impl RedisErrorExt for error_stack::Report { + fn to_redis_failed_response(self, key: &str) -> error_stack::Report { + match self.current_context() { + errors::RedisError::NotFound => self.change_context( + errors::StorageError::ValueNotFound(format!("Data does not exist for key {key}",)), + ), + errors::RedisError::SetNxFailed => { + self.change_context(errors::StorageError::DuplicateValue { + entity: "redis", + key: Some(key.to_string()), + }) + } + _ => self.change_context(errors::StorageError::KVError), + } + } +} diff --git a/crates/router/src/core/files.rs b/crates/router/src/core/files.rs index 13c4d3dfdf31..d3f490a0a6ce 100644 --- a/crates/router/src/core/files.rs +++ b/crates/router/src/core/files.rs @@ -1,9 +1,4 @@ pub mod helpers; -#[cfg(feature = "s3")] -pub mod s3_utils; - -#[cfg(not(feature = "s3"))] -pub mod fs_utils; use api_models::files; use error_stack::{IntoReport, ResultExt}; @@ -29,10 +24,7 @@ pub async fn files_create_core( ) .await?; let file_id = common_utils::generate_id(consts::ID_LENGTH, "file"); - #[cfg(feature = "s3")] let file_key = format!("{}/{}", merchant_account.merchant_id, file_id); - #[cfg(not(feature = "s3"))] - let file_key = format!("{}_{}", merchant_account.merchant_id, file_id); let file_new = diesel_models::file::FileMetadataNew { file_id: file_id.clone(), merchant_id: merchant_account.merchant_id.clone(), diff --git a/crates/router/src/core/files/fs_utils.rs b/crates/router/src/core/files/fs_utils.rs deleted file mode 100644 index 795f2fad7591..000000000000 --- a/crates/router/src/core/files/fs_utils.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::{ - fs::{remove_file, File}, - io::{Read, Write}, - path::PathBuf, -}; - -use common_utils::errors::CustomResult; -use error_stack::{IntoReport, ResultExt}; - -use crate::{core::errors, env}; - -pub fn get_file_path(file_key: String) -> PathBuf { - let mut file_path = PathBuf::new(); - file_path.push(env::workspace_path()); - file_path.push("files"); - file_path.push(file_key); - file_path -} - -pub fn save_file_to_fs( - file_key: String, - file_data: Vec, -) -> CustomResult<(), errors::ApiErrorResponse> { - let file_path = get_file_path(file_key); - let mut file = File::create(file_path) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to create file")?; - file.write_all(&file_data) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while writing into file")?; - Ok(()) -} - -pub fn delete_file_from_fs(file_key: String) -> CustomResult<(), errors::ApiErrorResponse> { - let file_path = get_file_path(file_key); - remove_file(file_path) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while deleting the file")?; - Ok(()) -} - -pub fn retrieve_file_from_fs(file_key: String) -> CustomResult, errors::ApiErrorResponse> { - let mut received_data: Vec = Vec::new(); - let file_path = get_file_path(file_key); - let mut file = File::open(file_path) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while opening the file")?; - file.read_to_end(&mut received_data) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while reading the file")?; - Ok(received_data) -} diff --git a/crates/router/src/core/files/helpers.rs b/crates/router/src/core/files/helpers.rs index 818067207f40..0a509b238afa 100644 --- a/crates/router/src/core/files/helpers.rs +++ b/crates/router/src/core/files/helpers.rs @@ -6,7 +6,7 @@ use futures::TryStreamExt; use crate::{ core::{ errors::{self, StorageErrorExt}, - files, payments, utils, + payments, utils, }, routes::AppState, services, @@ -30,37 +30,6 @@ pub async fn get_file_purpose(field: &mut Field) -> Option { } } -pub async fn upload_file( - #[cfg(feature = "s3")] state: &AppState, - file_key: String, - file: Vec, -) -> CustomResult<(), errors::ApiErrorResponse> { - #[cfg(feature = "s3")] - return files::s3_utils::upload_file_to_s3(state, file_key, file).await; - #[cfg(not(feature = "s3"))] - return files::fs_utils::save_file_to_fs(file_key, file); -} - -pub async fn delete_file( - #[cfg(feature = "s3")] state: &AppState, - file_key: String, -) -> CustomResult<(), errors::ApiErrorResponse> { - #[cfg(feature = "s3")] - return files::s3_utils::delete_file_from_s3(state, file_key).await; - #[cfg(not(feature = "s3"))] - return files::fs_utils::delete_file_from_fs(file_key); -} - -pub async fn retrieve_file( - #[cfg(feature = "s3")] state: &AppState, - file_key: String, -) -> CustomResult, errors::ApiErrorResponse> { - #[cfg(feature = "s3")] - return files::s3_utils::retrieve_file_from_s3(state, file_key).await; - #[cfg(not(feature = "s3"))] - return files::fs_utils::retrieve_file_from_fs(file_key); -} - pub async fn validate_file_upload( state: &AppState, merchant_account: domain::MerchantAccount, @@ -132,14 +101,11 @@ pub async fn delete_file_using_file_id( .attach_printable("File not available")?, }; match provider { - diesel_models::enums::FileUploadProvider::Router => { - delete_file( - #[cfg(feature = "s3")] - state, - provider_file_id, - ) + diesel_models::enums::FileUploadProvider::Router => state + .file_storage_client + .delete_file(&provider_file_id) .await - } + .change_context(errors::ApiErrorResponse::InternalServerError), _ => Err(errors::ApiErrorResponse::FileProviderNotSupported { message: "Not Supported because provider is not Router".to_string(), } @@ -234,12 +200,11 @@ pub async fn retrieve_file_and_provider_file_id_from_file_id( match provider { diesel_models::enums::FileUploadProvider::Router => Ok(( Some( - retrieve_file( - #[cfg(feature = "s3")] - state, - provider_file_id.clone(), - ) - .await?, + state + .file_storage_client + .retrieve_file(&provider_file_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?, ), Some(provider_file_id), )), @@ -364,13 +329,11 @@ pub async fn upload_and_get_provider_provider_file_id_profile_id( payment_attempt.merchant_connector_id, )) } else { - upload_file( - #[cfg(feature = "s3")] - state, - file_key.clone(), - create_file_request.file.clone(), - ) - .await?; + state + .file_storage_client + .upload_file(&file_key, create_file_request.file.clone()) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; Ok(( file_key, api_models::enums::FileUploadProvider::Router, diff --git a/crates/router/src/core/files/s3_utils.rs b/crates/router/src/core/files/s3_utils.rs deleted file mode 100644 index 228c23528cd5..000000000000 --- a/crates/router/src/core/files/s3_utils.rs +++ /dev/null @@ -1,87 +0,0 @@ -use aws_config::{self, meta::region::RegionProviderChain}; -use aws_sdk_s3::{config::Region, Client}; -use common_utils::errors::CustomResult; -use error_stack::{IntoReport, ResultExt}; -use futures::TryStreamExt; - -use crate::{core::errors, routes}; - -async fn get_aws_client(state: &routes::AppState) -> Client { - let region_provider = - RegionProviderChain::first_try(Region::new(state.conf.file_upload_config.region.clone())); - let sdk_config = aws_config::from_env().region(region_provider).load().await; - Client::new(&sdk_config) -} - -pub async fn upload_file_to_s3( - state: &routes::AppState, - file_key: String, - file: Vec, -) -> CustomResult<(), errors::ApiErrorResponse> { - let client = get_aws_client(state).await; - let bucket_name = &state.conf.file_upload_config.bucket_name; - // Upload file to S3 - let upload_res = client - .put_object() - .bucket(bucket_name) - .key(file_key.clone()) - .body(file.into()) - .send() - .await; - upload_res - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("File upload to S3 failed")?; - Ok(()) -} - -pub async fn delete_file_from_s3( - state: &routes::AppState, - file_key: String, -) -> CustomResult<(), errors::ApiErrorResponse> { - let client = get_aws_client(state).await; - let bucket_name = &state.conf.file_upload_config.bucket_name; - // Delete file from S3 - let delete_res = client - .delete_object() - .bucket(bucket_name) - .key(file_key) - .send() - .await; - delete_res - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("File delete from S3 failed")?; - Ok(()) -} - -pub async fn retrieve_file_from_s3( - state: &routes::AppState, - file_key: String, -) -> CustomResult, errors::ApiErrorResponse> { - let client = get_aws_client(state).await; - let bucket_name = &state.conf.file_upload_config.bucket_name; - // Get file data from S3 - let get_res = client - .get_object() - .bucket(bucket_name) - .key(file_key) - .send() - .await; - let mut object = get_res - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("File retrieve from S3 failed")?; - let mut received_data: Vec = Vec::new(); - while let Some(bytes) = object - .body - .try_next() - .await - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Invalid file data received from S3")? - { - received_data.extend_from_slice(&bytes); // Collect the bytes in the Vec - } - Ok(received_data) -} diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs new file mode 100644 index 000000000000..0e3f67c051b8 --- /dev/null +++ b/crates/router/src/core/fraud_check.rs @@ -0,0 +1,778 @@ +use std::fmt::Debug; + +use api_models::{admin::FrmConfigs, enums as api_enums, payments::AdditionalPaymentData}; +use error_stack::ResultExt; +use masking::{ExposeInterface, PeekInterface}; +use router_env::{ + logger, + tracing::{self, instrument}, +}; + +use self::{ + flows::{self as frm_flows, FeatureFrm}, + types::{ + self as frm_core_types, ConnectorDetailsCore, FrmConfigsObject, FrmData, FrmInfo, + PaymentDetails, PaymentToFrmData, + }, +}; +use super::errors::{ConnectorErrorExt, RouterResponse}; +use crate::{ + core::{ + errors::{self, RouterResult}, + payments::{ + self, flows::ConstructFlowSpecificData, helpers::get_additional_payment_data, + operations::BoxedOperation, + }, + utils as core_utils, + }, + db::StorageInterface, + routes::AppState, + services, + types::{ + self as oss_types, + api::{routing::FrmRoutingAlgorithm, Connector, FraudCheckConnectorData, Fulfillment}, + domain, fraud_check as frm_types, + storage::{ + enums::{ + AttemptStatus, FraudCheckLastStep, FraudCheckStatus, FraudCheckType, FrmSuggestion, + IntentStatus, + }, + fraud_check::{FraudCheck, FraudCheckUpdate}, + PaymentIntent, + }, + }, + utils::ValueExt, +}; +pub mod flows; +pub mod operation; +pub mod types; + +#[instrument(skip_all)] +pub async fn call_frm_service( + state: &AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, +) -> RouterResult> +where + F: Send + Clone, + + // To create connector flow specific interface data + FrmData: ConstructFlowSpecificData, + oss_types::RouterData: FeatureFrm + Send, + + // To construct connector flow specific api + dyn Connector: services::api::ConnectorIntegration, +{ + let merchant_connector_account = payments::construct_profile_id_and_get_mca( + state, + merchant_account, + payment_data, + &frm_data.connector_details.connector_name, + None, + key_store, + false, + ) + .await?; + + frm_data.payment_attempt.connector_transaction_id = payment_data + .payment_attempt + .connector_transaction_id + .clone(); + + let mut router_data = frm_data + .construct_router_data( + state, + &frm_data.connector_details.connector_name, + merchant_account, + key_store, + customer, + &merchant_connector_account, + ) + .await?; + + router_data.status = payment_data.payment_attempt.status; + + let connector = + FraudCheckConnectorData::get_connector_by_name(&frm_data.connector_details.connector_name)?; + let router_data_res = router_data + .decide_frm_flows( + state, + &connector, + payments::CallConnectorAction::Trigger, + merchant_account, + ) + .await?; + + Ok(router_data_res) +} + +pub async fn should_call_frm( + merchant_account: &domain::MerchantAccount, + payment_data: &payments::PaymentData, + db: &dyn StorageInterface, + key_store: domain::MerchantKeyStore, +) -> RouterResult<( + bool, + Option, + Option, + Option, +)> +where + F: Send + Clone, +{ + match merchant_account.frm_routing_algorithm.clone() { + Some(frm_routing_algorithm_value) => { + let frm_routing_algorithm_struct: FrmRoutingAlgorithm = frm_routing_algorithm_value + .clone() + .parse_value("FrmRoutingAlgorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "frm_routing_algorithm", + }) + .attach_printable("Data field not found in frm_routing_algorithm")?; + + let profile_id = core_utils::get_profile_id_from_business_details( + payment_data.payment_intent.business_country, + payment_data.payment_intent.business_label.as_ref(), + merchant_account, + payment_data.payment_intent.profile_id.as_ref(), + db, + false, + ) + .await + .attach_printable("Could not find profile id from business details")?; + + let merchant_connector_account_from_db_option = db + .find_merchant_connector_account_by_profile_id_connector_name( + &profile_id, + &frm_routing_algorithm_struct.data, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + }) + .ok(); + + match merchant_connector_account_from_db_option { + Some(merchant_connector_account_from_db) => { + let frm_configs_option = merchant_connector_account_from_db + .frm_configs + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "frm_configs", + }) + .ok(); + match frm_configs_option { + Some(frm_configs_value) => { + let frm_configs_struct: Vec = frm_configs_value + .into_iter() + .map(|config| { config + .expose() + .parse_value("FrmConfigs") + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "frm_configs".to_string(), + expected_format: r#"[{ "gateway": "stripe", "payment_methods": [{ "payment_method": "card","payment_method_types": [{"payment_method_type": "credit","card_networks": ["Visa"],"flow": "pre","action": "cancel_txn"}]}]}]"#.to_string(), + }) + }) + .collect::, _>>()?; + + let mut is_frm_connector_enabled = false; + let mut is_frm_pm_enabled = false; + let mut is_frm_pmt_enabled = false; + let filtered_frm_config = frm_configs_struct + .iter() + .filter(|frm_config| { + match ( + &payment_data.clone().payment_attempt.connector, + &frm_config.gateway, + ) { + (Some(current_connector), Some(configured_connector)) => { + let is_enabled = *current_connector + == configured_connector.to_string(); + if is_enabled { + is_frm_connector_enabled = true; + } + is_enabled + } + (None, _) | (_, None) => true, + } + }) + .collect::>(); + let filtered_payment_methods = filtered_frm_config + .iter() + .map(|frm_config| { + let filtered_frm_config_by_pm = frm_config + .payment_methods + .iter() + .filter(|frm_config_pm| { + match ( + payment_data.payment_attempt.payment_method, + frm_config_pm.payment_method, + ) { + ( + Some(current_pm), + Some(configured_connector_pm), + ) => { + let is_enabled = current_pm.to_string() + == configured_connector_pm.to_string(); + if is_enabled { + is_frm_pm_enabled = true; + } + is_enabled + } + (None, _) | (_, None) => true, + } + }) + .collect::>(); + filtered_frm_config_by_pm + }) + .collect::>() + .concat(); + let additional_payment_data = match &payment_data.payment_method_data { + Some(pmd) => { + let additional_payment_data = + get_additional_payment_data(pmd, db).await; + Some(additional_payment_data) + } + None => payment_data + .payment_attempt + .payment_method_data + .as_ref() + .map(|pm_data| { + pm_data.clone().parse_value::( + "AdditionalPaymentData", + ) + }) + .transpose() + .unwrap_or_default(), // Making this default in case of error as we don't want to fail payment for frm errors + }; + let filtered_payment_method_types = filtered_payment_methods + .iter() + .map(|frm_pm_config| { + let filtered_pm_config_by_pmt = frm_pm_config + .payment_method_types + .iter() + .filter(|frm_pm_config_by_pmt| { + match ( + &payment_data + .clone() + .payment_attempt + .payment_method_type, + frm_pm_config_by_pmt.payment_method_type, + ) { + (Some(curr), Some(conf)) + if curr.to_string() == conf.to_string() => + { + is_frm_pmt_enabled = true; + true + } + (None, Some(conf)) => match additional_payment_data + .clone() + { + Some(AdditionalPaymentData::Card(card)) => { + let card_type = card + .card_type + .unwrap_or_else(|| "debit".to_string()); + let is_enabled = card_type.to_lowercase() + == conf.to_string().to_lowercase(); + if is_enabled { + is_frm_pmt_enabled = true; + } + is_enabled + } + _ => false, + }, + _ => false, + } + }) + .collect::>(); + filtered_pm_config_by_pmt + }) + .collect::>() + .concat(); + let is_frm_enabled = + is_frm_connector_enabled && is_frm_pm_enabled && is_frm_pmt_enabled; + logger::debug!( + "frm_configs {:?} {:?} {:?} {:?}", + is_frm_connector_enabled, + is_frm_pm_enabled, + is_frm_pmt_enabled, + is_frm_enabled + ); + // filtered_frm_config... + // Panic Safety: we are first checking if the object is present... only if present, we try to fetch index 0 + let frm_configs_object = FrmConfigsObject { + frm_enabled_gateway: filtered_frm_config + .first() + .and_then(|c| c.gateway), + frm_enabled_pm: filtered_payment_methods + .first() + .and_then(|pm| pm.payment_method), + frm_enabled_pm_type: filtered_payment_method_types + .first() + .and_then(|pmt| pmt.payment_method_type), + frm_action: filtered_payment_method_types + // .clone() + .first() + .map(|pmt| pmt.action.clone()) + .unwrap_or(api_enums::FrmAction::ManualReview), + frm_preferred_flow_type: filtered_payment_method_types + .first() + .map(|pmt| pmt.flow.clone()) + .unwrap_or(api_enums::FrmPreferredFlowTypes::Pre), + }; + logger::debug!( + "frm_routing_configs: {:?} {:?} {:?} {:?}", + frm_routing_algorithm_struct, + profile_id, + frm_configs_object, + is_frm_enabled + ); + Ok(( + is_frm_enabled, + Some(frm_routing_algorithm_struct), + Some(profile_id.to_string()), + Some(frm_configs_object), + )) + } + None => { + logger::error!("Cannot find frm_configs for FRM provider"); + Ok((false, None, None, None)) + } + } + } + None => { + logger::error!("Cannot find merchant connector account for FRM provider"); + Ok((false, None, None, None)) + } + } + } + _ => Ok((false, None, None, None)), + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn make_frm_data_and_fraud_check_operation<'a, F>( + _db: &dyn StorageInterface, + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: payments::PaymentData, + frm_routing_algorithm: FrmRoutingAlgorithm, + profile_id: String, + frm_configs: FrmConfigsObject, + _customer: &Option, +) -> RouterResult> +where + F: Send + Clone, +{ + let order_details = payment_data + .payment_intent + .order_details + .clone() + .or_else(|| + // when the order_details are present within the meta_data, we need to take those to support backward compatibility + payment_data.payment_intent.metadata.clone().and_then(|meta| { + let order_details = meta.peek().get("order_details").to_owned(); + order_details.map(|order| vec![masking::Secret::new(order.to_owned())]) + })) + .map(|order_details_value| { + order_details_value + .into_iter() + .map(|data| { + data.peek() + .to_owned() + .parse_value("OrderDetailsWithAmount") + .attach_printable("unable to parse OrderDetailsWithAmount") + }) + .collect::, _>>() + .unwrap_or_default() + }); + + let frm_connector_details = ConnectorDetailsCore { + connector_name: frm_routing_algorithm.data, + profile_id, + }; + + let payment_to_frm_data = PaymentToFrmData { + amount: payment_data.amount, + payment_intent: payment_data.payment_intent, + payment_attempt: payment_data.payment_attempt, + merchant_account: merchant_account.to_owned(), + address: payment_data.address.clone(), + connector_details: frm_connector_details.clone(), + order_details, + frm_metadata: payment_data.frm_metadata.clone(), + }; + + let fraud_check_operation: operation::BoxedFraudCheckOperation = + match frm_configs.frm_preferred_flow_type { + api_enums::FrmPreferredFlowTypes::Pre => Box::new(operation::FraudCheckPre), + api_enums::FrmPreferredFlowTypes::Post => Box::new(operation::FraudCheckPost), + }; + let frm_data = fraud_check_operation + .to_get_tracker()? + .get_trackers(state, payment_to_frm_data, frm_connector_details) + .await?; + Ok(FrmInfo { + fraud_check_operation, + frm_data, + suggested_action: None, + }) +} + +#[allow(clippy::too_many_arguments)] +pub async fn pre_payment_frm_core<'a, F>( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut payments::PaymentData, + frm_info: &mut FrmInfo, + frm_configs: FrmConfigsObject, + customer: &Option, + should_continue_transaction: &mut bool, + should_continue_capture: &mut bool, + key_store: domain::MerchantKeyStore, +) -> RouterResult> +where + F: Send + Clone, +{ + if let Some(frm_data) = &mut frm_info.frm_data { + if matches!( + frm_configs.frm_preferred_flow_type, + api_enums::FrmPreferredFlowTypes::Pre + ) { + let fraud_check_operation = &mut frm_info.fraud_check_operation; + + let frm_router_data = fraud_check_operation + .to_domain()? + .pre_payment_frm( + state, + payment_data, + frm_data, + merchant_account, + customer, + key_store, + ) + .await?; + let frm_data_updated = fraud_check_operation + .to_update_tracker()? + .update_tracker( + &*state.store, + frm_data.clone(), + payment_data, + None, + frm_router_data, + ) + .await?; + let frm_fraud_check = frm_data_updated.fraud_check.clone(); + payment_data.frm_message = Some(frm_fraud_check.clone()); + if matches!(frm_fraud_check.frm_status, FraudCheckStatus::Fraud) { + if matches!(frm_configs.frm_action, api_enums::FrmAction::CancelTxn) { + *should_continue_transaction = false; + frm_info.suggested_action = Some(FrmSuggestion::FrmCancelTransaction); + } else if matches!(frm_configs.frm_action, api_enums::FrmAction::ManualReview) { + *should_continue_capture = false; + frm_info.suggested_action = Some(FrmSuggestion::FrmManualReview); + } + } + logger::debug!( + "frm_updated_data: {:?} {:?}", + frm_info.fraud_check_operation, + frm_info.suggested_action + ); + Ok(Some(frm_data_updated)) + } else { + Ok(Some(frm_data.to_owned())) + } + } else { + Ok(None) + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn post_payment_frm_core<'a, F>( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut payments::PaymentData, + frm_info: &mut FrmInfo, + frm_configs: FrmConfigsObject, + customer: &Option, + key_store: domain::MerchantKeyStore, +) -> RouterResult> +where + F: Send + Clone, +{ + if let Some(frm_data) = &mut frm_info.frm_data { + // Allow the Post flow only if the payment is succeeded, + // this logic has to be removed if we are going to call /sale or /transaction after failed transaction + let fraud_check_operation = &mut frm_info.fraud_check_operation; + if payment_data.payment_attempt.status == AttemptStatus::Charged { + let frm_router_data_opt = fraud_check_operation + .to_domain()? + .post_payment_frm( + state, + payment_data, + frm_data, + merchant_account, + customer, + key_store.clone(), + ) + .await?; + if let Some(frm_router_data) = frm_router_data_opt { + let mut frm_data = fraud_check_operation + .to_update_tracker()? + .update_tracker( + &*state.store, + frm_data.to_owned(), + payment_data, + None, + frm_router_data.to_owned(), + ) + .await?; + + payment_data.frm_message = Some(frm_data.fraud_check.clone()); + logger::debug!( + "frm_updated_data: {:?} {:?}", + frm_data, + payment_data.frm_message + ); + let mut frm_suggestion = None; + fraud_check_operation + .to_domain()? + .execute_post_tasks( + state, + &mut frm_data, + merchant_account, + frm_configs, + &mut frm_suggestion, + key_store, + payment_data, + customer, + ) + .await?; + logger::debug!("frm_post_tasks_data: {:?}", frm_data); + let updated_frm_data = fraud_check_operation + .to_update_tracker()? + .update_tracker( + &*state.store, + frm_data.to_owned(), + payment_data, + frm_suggestion, + frm_router_data.to_owned(), + ) + .await?; + return Ok(Some(updated_frm_data)); + } + } + + Ok(Some(frm_data.to_owned())) + } else { + Ok(None) + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn call_frm_before_connector_call<'a, F, Req, Ctx>( + db: &dyn StorageInterface, + operation: &BoxedOperation<'_, F, Req, Ctx>, + merchant_account: &domain::MerchantAccount, + payment_data: &mut payments::PaymentData, + state: &AppState, + frm_info: &mut Option>, + customer: &Option, + should_continue_transaction: &mut bool, + should_continue_capture: &mut bool, + key_store: domain::MerchantKeyStore, +) -> RouterResult> +where + F: Send + Clone, +{ + if is_operation_allowed(operation) { + let (is_frm_enabled, frm_routing_algorithm, frm_connector_label, frm_configs) = + should_call_frm(merchant_account, payment_data, db, key_store.clone()).await?; + if let Some((frm_routing_algorithm_val, profile_id)) = + frm_routing_algorithm.zip(frm_connector_label) + { + if let Some(frm_configs) = frm_configs.clone() { + let mut updated_frm_info = make_frm_data_and_fraud_check_operation( + db, + state, + merchant_account, + payment_data.to_owned(), + frm_routing_algorithm_val, + profile_id, + frm_configs.clone(), + customer, + ) + .await?; + + if is_frm_enabled { + pre_payment_frm_core( + state, + merchant_account, + payment_data, + &mut updated_frm_info, + frm_configs, + customer, + should_continue_transaction, + should_continue_capture, + key_store, + ) + .await?; + } + *frm_info = Some(updated_frm_info); + } + } + logger::debug!("frm_configs: {:?} {:?}", frm_configs, is_frm_enabled); + return Ok(frm_configs); + } + Ok(None) +} + +pub fn is_operation_allowed(operation: &Op) -> bool { + !["PaymentSession", "PaymentApprove", "PaymentReject"] + .contains(&format!("{operation:?}").as_str()) +} + +impl From for PaymentDetails { + fn from(payment_data: PaymentToFrmData) -> Self { + Self { + amount: payment_data.amount.into(), + currency: payment_data.payment_attempt.currency, + payment_method: payment_data.payment_attempt.payment_method, + payment_method_type: payment_data.payment_attempt.payment_method_type, + refund_transaction_id: None, + } + } +} + +#[instrument(skip_all)] +pub async fn frm_fulfillment_core( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + req: frm_core_types::FrmFulfillmentRequest, +) -> RouterResponse { + let db = &*state.clone().store; + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id( + &req.payment_id.clone(), + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + match payment_intent.status { + IntentStatus::Succeeded => { + let invalid_request_error = errors::ApiErrorResponse::InvalidRequestData { + message: "no fraud check entry found for this payment_id".to_string(), + }; + let existing_fraud_check = db + .find_fraud_check_by_payment_id_if_present( + req.payment_id.clone(), + merchant_account.merchant_id.clone(), + ) + .await + .change_context(invalid_request_error.to_owned())?; + match existing_fraud_check { + Some(fraud_check) => { + if (matches!(fraud_check.frm_transaction_type, FraudCheckType::PreFrm) + && fraud_check.last_step == FraudCheckLastStep::TransactionOrRecordRefund) + || (matches!(fraud_check.frm_transaction_type, FraudCheckType::PostFrm) + && fraud_check.last_step == FraudCheckLastStep::CheckoutOrSale) + { + Box::pin(make_fulfillment_api_call( + db, + fraud_check, + payment_intent, + state, + merchant_account, + key_store, + req, + )) + .await + } else { + Err(errors::ApiErrorResponse::PreconditionFailed {message:"Frm pre/post flow hasn't terminated yet, so fulfillment cannot be called".to_string(),}.into()) + } + } + None => Err(invalid_request_error.into()), + } + } + _ => Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Fulfillment can be performed only for succeeded payment".to_string(), + } + .into()), + } +} + +#[instrument(skip_all)] +pub async fn make_fulfillment_api_call( + db: &dyn StorageInterface, + fraud_check: FraudCheck, + payment_intent: PaymentIntent, + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + req: frm_core_types::FrmFulfillmentRequest, +) -> RouterResponse { + let payment_attempt = db + .find_payment_attempt_by_attempt_id_merchant_id( + &payment_intent.active_attempt.get_id(), + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + let connector_data = FraudCheckConnectorData::get_connector_by_name(&fraud_check.frm_name)?; + let connector_integration: services::BoxedConnectorIntegration< + '_, + Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > = connector_data.connector.get_connector_integration(); + let router_data = frm_flows::fulfillment_flow::construct_fulfillment_router_data( + &state, + &payment_intent, + &payment_attempt, + &merchant_account, + &key_store, + fraud_check.frm_name.clone(), + req, + ) + .await?; + let response = services::execute_connector_processing_step( + &state, + connector_integration, + &router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_payment_failed_response()?; + let fraud_check_copy = fraud_check.clone(); + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: fraud_check.frm_status, + frm_transaction_id: fraud_check.frm_transaction_id, + frm_reason: fraud_check.frm_reason, + frm_score: fraud_check.frm_score, + metadata: fraud_check.metadata, + modified_at: common_utils::date_time::now(), + last_step: FraudCheckLastStep::Fulfillment, + }; + let _updated = db + .update_fraud_check_response_with_attempt_id(fraud_check_copy, fraud_check_update) + .await + .map_err(|error| error.change_context(errors::ApiErrorResponse::PaymentNotFound))?; + let fulfillment_response = + response + .response + .map_err(|err| errors::ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector_data.connector_name.clone().to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + Ok(services::ApplicationResponse::Json(fulfillment_response)) +} diff --git a/crates/router/src/core/fraud_check/flows.rs b/crates/router/src/core/fraud_check/flows.rs new file mode 100644 index 000000000000..3d4916a372be --- /dev/null +++ b/crates/router/src/core/fraud_check/flows.rs @@ -0,0 +1,36 @@ +pub mod checkout_flow; +pub mod fulfillment_flow; +pub mod record_return; +pub mod sale_flow; +pub mod transaction_flow; + +use async_trait::async_trait; + +use crate::{ + core::{ + errors::RouterResult, + payments::{self, flows::ConstructFlowSpecificData}, + }, + routes::AppState, + services, + types::{ + api::{Connector, FraudCheckConnectorData}, + domain, + fraud_check::FraudCheckResponseData, + }, +}; + +#[async_trait] +pub trait FeatureFrm { + async fn decide_frm_flows<'a>( + self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult + where + Self: Sized, + F: Clone, + dyn Connector: services::ConnectorIntegration; +} diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs new file mode 100644 index 000000000000..8b3eed45f9e8 --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -0,0 +1,174 @@ +use async_trait::async_trait; +use common_utils::{ext_traits::ValueExt, pii::Email}; +use error_stack::ResultExt; +use masking::ExposeInterface; + +use super::{ConstructFlowSpecificData, FeatureFrm}; +use crate::{ + connector::utils::PaymentsAttemptData, + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::types::FrmData, + payments::{self, helpers}, + }, + errors, services, + types::{ + api::fraud_check::{self as frm_api, FraudCheckConnectorData}, + domain, + fraud_check::{FraudCheckCheckoutData, FraudCheckResponseData, FrmCheckoutRouterData}, + storage::enums as storage_enums, + BrowserInformation, ConnectorAuthType, ResponseId, RouterData, + }, + AppState, +}; + +#[async_trait] +impl ConstructFlowSpecificData + for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult> + { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let browser_info: Option = self.payment_attempt.get_browser_info().ok(); + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: self + .payment_attempt + .payment_method + .ok_or(errors::ApiErrorResponse::PaymentMethodNotFound)?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckCheckoutData { + amount: self.payment_attempt.amount, + order_details: self.order_details.clone(), + currency: self.payment_attempt.currency, + browser_info, + payment_method_data: self + .payment_attempt + .payment_method_data + .as_ref() + .map(|pm_data| { + pm_data + .clone() + .parse_value::( + "AdditionalPaymentData", + ) + }) + .transpose() + .unwrap_or_default(), + email: customer.clone().and_then(|customer_data| { + customer_data + .email + .and_then(|email| Email::try_from(email.into_inner().expose()).ok()) + }), + gateway: self.payment_attempt.connector.clone(), + }, // self.order_details + response: Ok(FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + status: storage_enums::FraudCheckStatus::Pending, + score: None, + reason: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[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, + connector_api_version: None, + apple_pay_flow: None, + frm_metadata: self.frm_metadata.clone(), + refund_id: None, + dispute_id: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmCheckoutRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmCheckoutRouterData, + state: &'a AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + frm_api::Checkout, + FraudCheckCheckoutData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs b/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs new file mode 100644 index 000000000000..706a0f9142eb --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs @@ -0,0 +1,113 @@ +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; +use router_env::tracing::{self, instrument}; + +use crate::{ + core::{ + errors::RouterResult, + fraud_check::frm_core_types::FrmFulfillmentRequest, + payments::{helpers, PaymentAddress}, + utils as core_utils, + }, + errors, + types::{ + domain, + fraud_check::{FraudCheckFulfillmentData, FrmFulfillmentRouterData}, + storage, ConnectorAuthType, ErrorResponse, RouterData, + }, + utils, AppState, +}; + +#[instrument(skip_all)] +pub async fn construct_fulfillment_router_data<'a>( + state: &'a AppState, + payment_intent: &'a storage::PaymentIntent, + payment_attempt: &storage::PaymentAttempt, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + connector: String, + fulfillment_request: FrmFulfillmentRequest, +) -> RouterResult { + let profile_id = core_utils::get_profile_id_from_business_details( + payment_intent.business_country, + payment_intent.business_label.as_ref(), + merchant_account, + payment_intent.profile_id.as_ref(), + &*state.store, + false, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")?; + + let merchant_connector_account = helpers::get_merchant_connector_account( + state, + merchant_account.merchant_id.as_str(), + None, + key_store, + &profile_id, + &connector, + None, + ) + .await?; + + let test_mode: Option = merchant_connector_account.is_test_mode_on(); + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let payment_method = utils::OptionExt::get_required_value( + payment_attempt.payment_method, + "payment_method_type", + )?; + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + connector, + payment_id: payment_attempt.payment_id.clone(), + attempt_id: payment_attempt.attempt_id.clone(), + status: payment_attempt.status, + payment_method, + connector_auth_type: auth_type, + description: None, + return_url: payment_intent.return_url.clone(), + payment_method_id: payment_attempt.payment_method_id.clone(), + address: PaymentAddress::default(), + auth_type: payment_attempt.authentication_type.unwrap_or_default(), + connector_meta_data: merchant_connector_account.get_metadata(), + amount_captured: payment_intent.amount_captured, + request: FraudCheckFulfillmentData { + amount: payment_attempt.amount, + order_details: payment_intent.order_details.clone(), + fulfillment_req: fulfillment_request, + }, + response: Err(ErrorResponse::default()), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + customer_id: None, + recurring_mandate_payment_data: None, + preprocessing_id: None, + payment_method_balance: None, + connector_request_reference_id: core_utils::get_connector_request_reference_id( + &state.conf, + &merchant_account.merchant_id, + payment_attempt, + ), + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + test_mode, + connector_api_version: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + }; + Ok(router_data) +} diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs new file mode 100644 index 000000000000..2de6aaab7c73 --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -0,0 +1,152 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use crate::{ + connector::signifyd::transformers::RefundMethod, + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::{FeatureFrm, FraudCheckConnectorData, FrmData}, + payments::{self, flows::ConstructFlowSpecificData, helpers}, + }, + errors, services, + types::{ + api::RecordReturn, + domain, + fraud_check::{ + FraudCheckRecordReturnData, FraudCheckResponseData, FrmRecordReturnRouterData, + }, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + utils, AppState, +}; + +#[async_trait] +impl ConstructFlowSpecificData + for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult> + { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + let currency = self.payment_attempt.clone().currency; + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: utils::OptionExt::get_required_value( + self.payment_attempt.payment_method, + "payment_method_type", + )?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckRecordReturnData { + amount: self.payment_attempt.amount, + refund_method: RefundMethod::OriginalPaymentInstrument, //we dont consume this data now in payments...hence hardcoded + currency, + refund_transaction_id: self.refund.clone().map(|refund| refund.refund_id), + }, // self.order_details + response: Ok(FraudCheckResponseData::RecordReturnResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + return_id: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[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, + connector_api_version: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmRecordReturnRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmRecordReturnRouterData, + state: &'a AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + RecordReturn, + FraudCheckRecordReturnData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/flows/sale_flow.rs b/crates/router/src/core/fraud_check/flows/sale_flow.rs new file mode 100644 index 000000000000..dfe70570047e --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/sale_flow.rs @@ -0,0 +1,148 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::{FeatureFrm, FraudCheckConnectorData, FrmData}, + payments::{self, flows::ConstructFlowSpecificData, helpers}, + }, + errors, services, + types::{ + api::fraud_check as frm_api, + domain, + fraud_check::{FraudCheckResponseData, FraudCheckSaleData, FrmSaleRouterData}, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + AppState, +}; + +#[async_trait] +impl ConstructFlowSpecificData + for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult> { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: self + .payment_attempt + .payment_method + .ok_or(errors::ApiErrorResponse::PaymentMethodNotFound)?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckSaleData { + amount: self.payment_attempt.amount, + order_details: self.order_details.clone(), + }, + response: Ok(FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + status: storage_enums::FraudCheckStatus::Pending, + score: None, + reason: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[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, + connector_api_version: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmSaleRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmSaleRouterData, + state: &'a AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + frm_api::Sale, + FraudCheckSaleData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/flows/transaction_flow.rs b/crates/router/src/core/fraud_check/flows/transaction_flow.rs new file mode 100644 index 000000000000..a84d9a771b4d --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/transaction_flow.rs @@ -0,0 +1,164 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::{FeatureFrm, FrmData}, + payments::{self, flows::ConstructFlowSpecificData, helpers}, + }, + errors, services, + types::{ + api::fraud_check as frm_api, + domain, + fraud_check::{ + FraudCheckResponseData, FraudCheckTransactionData, FrmTransactionRouterData, + }, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + AppState, +}; + +#[async_trait] +impl + ConstructFlowSpecificData< + frm_api::Transaction, + FraudCheckTransactionData, + FraudCheckResponseData, + > for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult< + RouterData, + > { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + + let payment_method = self.payment_attempt.payment_method; + let currency = self.payment_attempt.currency; + + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: self + .payment_attempt + .payment_method + .ok_or(errors::ApiErrorResponse::PaymentMethodNotFound)?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckTransactionData { + amount: self.payment_attempt.amount, + order_details: self.order_details.clone(), + currency, + payment_method, + error_code: self.payment_attempt.error_code.clone(), + error_message: self.payment_attempt.error_message.clone(), + connector_transaction_id: self.payment_attempt.connector_transaction_id.clone(), + }, // self.order_details + response: Ok(FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + status: storage_enums::FraudCheckStatus::Pending, + score: None, + reason: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[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, + connector_api_version: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmTransactionRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &frm_api::FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmTransactionRouterData, + state: &'a AppState, + connector: &frm_api::FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + frm_api::Transaction, + FraudCheckTransactionData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/operation.rs b/crates/router/src/core/fraud_check/operation.rs new file mode 100644 index 000000000000..e7677dad6f3a --- /dev/null +++ b/crates/router/src/core/fraud_check/operation.rs @@ -0,0 +1,106 @@ +pub mod fraud_check_post; +pub mod fraud_check_pre; +use async_trait::async_trait; +use common_enums::FrmSuggestion; +use error_stack::{report, ResultExt}; + +pub use self::{fraud_check_post::FraudCheckPost, fraud_check_pre::FraudCheckPre}; +use super::{ + types::{ConnectorDetailsCore, FrmConfigsObject, PaymentToFrmData}, + FrmData, +}; +use crate::{ + core::{ + errors::{self, RouterResult}, + payments, + }, + db::StorageInterface, + routes::AppState, + types::{domain, fraud_check::FrmRouterData}, +}; + +pub type BoxedFraudCheckOperation = Box + Send + Sync>; + +pub trait FraudCheckOperation: Send + std::fmt::Debug { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + .attach_printable_lazy(|| format!("get tracker interface not found for {self:?}")) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + .attach_printable_lazy(|| format!("domain interface not found for {self:?}")) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + .attach_printable_lazy(|| format!("get tracker interface not found for {self:?}")) + } +} + +#[async_trait] +pub trait GetTracker: Send { + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_data: D, + frm_connector_details: ConnectorDetailsCore, + ) -> RouterResult>; +} + +#[async_trait] +pub trait Domain: Send + Sync { + async fn post_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult> + where + F: Send + Clone; + + async fn pre_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult + where + F: Send + Clone; + + // To execute several tasks conditionally based on the result of post_flow. + // Eg: If the /sale(post flow) is returning the transaction as fraud we can execute refund in post task + #[allow(clippy::too_many_arguments)] + async fn execute_post_tasks( + &self, + _state: &AppState, + frm_data: &mut FrmData, + _merchant_account: &domain::MerchantAccount, + _frm_configs: FrmConfigsObject, + _frm_suggestion: &mut Option, + _key_store: domain::MerchantKeyStore, + _payment_data: &mut payments::PaymentData, + _customer: &Option, + ) -> RouterResult> + where + F: Send + Clone, + { + return Ok(Some(frm_data.to_owned())); + } +} + +#[async_trait] +pub trait UpdateTracker: Send { + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + frm_data: D, + payment_data: &mut payments::PaymentData, + _frm_suggestion: Option, + frm_router_data: FrmRouterData, + ) -> RouterResult; +} diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_post.rs b/crates/router/src/core/fraud_check/operation/fraud_check_post.rs new file mode 100644 index 000000000000..8bb8a61402ea --- /dev/null +++ b/crates/router/src/core/fraud_check/operation/fraud_check_post.rs @@ -0,0 +1,458 @@ +use async_trait::async_trait; +use common_enums::FrmSuggestion; +use common_utils::ext_traits::Encode; +use data_models::payments::{ + payment_attempt::PaymentAttemptUpdate, payment_intent::PaymentIntentUpdate, +}; +use router_env::{instrument, logger, tracing}; + +use super::{Domain, FraudCheckOperation, GetTracker, UpdateTracker}; +use crate::{ + consts, + core::{ + errors::{RouterResult, StorageErrorExt}, + fraud_check::{ + self as frm_core, + types::{FrmData, PaymentDetails, PaymentToFrmData, REFUND_INITIATED}, + ConnectorDetailsCore, FrmConfigsObject, + }, + payments, refunds, + }, + db::StorageInterface, + errors, services, + types::{ + api::{ + enums::{AttemptStatus, FrmAction, IntentStatus}, + fraud_check as frm_api, + refunds::{RefundRequest, RefundType}, + }, + domain, + fraud_check::{ + FraudCheckResponseData, FraudCheckSaleData, FrmRequest, FrmResponse, FrmRouterData, + }, + storage::{ + enums::{FraudCheckLastStep, FraudCheckStatus, FraudCheckType, MerchantDecision}, + fraud_check::{FraudCheckNew, FraudCheckUpdate}, + }, + ResponseId, + }, + utils, AppState, +}; + +#[derive(Debug, Clone, Copy)] +pub struct FraudCheckPost; + +impl FraudCheckOperation for &FraudCheckPost { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(*self) + } +} + +impl FraudCheckOperation for FraudCheckPost { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(self) + } +} + +#[async_trait] +impl GetTracker for FraudCheckPost { + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_data: PaymentToFrmData, + frm_connector_details: ConnectorDetailsCore, + ) -> RouterResult> { + let db = &*state.store; + + let payment_details: Option = + Encode::::encode_to_value(&PaymentDetails::from(payment_data.clone())) + .ok(); + let existing_fraud_check = db + .find_fraud_check_by_payment_id_if_present( + payment_data.payment_intent.payment_id.clone(), + payment_data.merchant_account.merchant_id.clone(), + ) + .await + .ok(); + let fraud_check = match existing_fraud_check { + Some(Some(fraud_check)) => Ok(fraud_check), + _ => { + db.insert_fraud_check_response(FraudCheckNew { + frm_id: utils::generate_id(consts::ID_LENGTH, "frm"), + payment_id: payment_data.payment_intent.payment_id.clone(), + merchant_id: payment_data.merchant_account.merchant_id.clone(), + attempt_id: payment_data.payment_attempt.attempt_id.clone(), + created_at: common_utils::date_time::now(), + frm_name: frm_connector_details.connector_name, + frm_transaction_id: None, + frm_transaction_type: FraudCheckType::PostFrm, + frm_status: FraudCheckStatus::Pending, + frm_score: None, + frm_reason: None, + frm_error: None, + payment_details, + metadata: None, + modified_at: common_utils::date_time::now(), + last_step: FraudCheckLastStep::Processing, + }) + .await + } + }; + match fraud_check { + Ok(fraud_check_value) => { + let frm_data = FrmData { + payment_intent: payment_data.payment_intent, + payment_attempt: payment_data.payment_attempt, + merchant_account: payment_data.merchant_account, + address: payment_data.address, + fraud_check: fraud_check_value, + connector_details: payment_data.connector_details, + order_details: payment_data.order_details, + refund: None, + frm_metadata: payment_data.frm_metadata, + }; + Ok(Some(frm_data)) + } + Err(error) => { + router_env::logger::error!("inserting into fraud_check table failed {error:?}"); + Ok(None) + } + } + } +} + +#[async_trait] +impl Domain for FraudCheckPost { + #[instrument(skip_all)] + async fn post_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult> { + if frm_data.fraud_check.last_step != FraudCheckLastStep::Processing { + logger::debug!("post_flow::Sale Skipped"); + return Ok(None); + } + let router_data = frm_core::call_frm_service::( + state, + payment_data, + &mut frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::CheckoutOrSale; + Ok(Some(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Sale(FraudCheckSaleData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + }), + response: FrmResponse::Sale(router_data.response), + })) + } + + #[instrument(skip_all)] + async fn execute_post_tasks( + &self, + state: &AppState, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + frm_configs: FrmConfigsObject, + frm_suggestion: &mut Option, + key_store: domain::MerchantKeyStore, + payment_data: &mut payments::PaymentData, + customer: &Option, + ) -> RouterResult> { + if matches!(frm_data.fraud_check.frm_status, FraudCheckStatus::Fraud) + && matches!(frm_configs.frm_action, FrmAction::AutoRefund) + && matches!( + frm_data.fraud_check.last_step, + FraudCheckLastStep::CheckoutOrSale + ) + { + *frm_suggestion = Some(FrmSuggestion::FrmAutoRefund); + let ref_req = RefundRequest { + refund_id: None, + payment_id: payment_data.payment_intent.payment_id.clone(), + merchant_id: Some(merchant_account.merchant_id.clone()), + amount: None, + reason: frm_data + .fraud_check + .frm_reason + .clone() + .map(|data| data.to_string()), + refund_type: Some(RefundType::Instant), + metadata: None, + merchant_connector_details: None, + }; + let refund = Box::pin(refunds::refund_create_core( + state.clone(), + merchant_account.clone(), + key_store.clone(), + ref_req, + )) + .await?; + if let services::ApplicationResponse::Json(new_refund) = refund { + frm_data.refund = Some(new_refund); + } + let _router_data = frm_core::call_frm_service::( + state, + payment_data, + &mut frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::TransactionOrRecordRefund; + }; + return Ok(Some(frm_data.to_owned())); + } + + #[instrument(skip_all)] + async fn pre_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult { + let router_data = frm_core::call_frm_service::( + state, + payment_data, + &mut frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + Ok(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Sale(FraudCheckSaleData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + }), + response: FrmResponse::Sale(router_data.response), + }) + } +} + +#[async_trait] +impl UpdateTracker for FraudCheckPost { + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + mut frm_data: FrmData, + payment_data: &mut payments::PaymentData, + frm_suggestion: Option, + frm_router_data: FrmRouterData, + ) -> RouterResult { + let frm_check_update = match frm_router_data.response { + FrmResponse::Sale(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + }, + FraudCheckResponseData::RecordReturnResponse { resource_id: _, connector_metadata: _, return_id: _ } => { + Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Record Return Response response in current Sale flow".to_string(), + )), + }) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + }, + }, + FrmResponse::Fulfillment(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + FraudCheckResponseData::RecordReturnResponse { resource_id: _, connector_metadata: _, return_id: _ } => None, + + }, + }, + + FrmResponse::RecordReturn(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id: _, + connector_metadata: _, + status: _, + reason: _, + score: _, + } => { + Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Transaction Response response in current Record Return flow".to_string(), + )), + }) + }, + FraudCheckResponseData::FulfillmentResponse {order_id: _, shipment_ids: _ } => { + None + }, + FraudCheckResponseData::RecordReturnResponse { resource_id, connector_metadata, return_id: _ } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: frm_data.fraud_check.frm_status, + frm_transaction_id: connector_transaction_id, + frm_reason: frm_data.fraud_check.frm_reason.clone(), + frm_score: frm_data.fraud_check.frm_score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + + } + }, + }, + + + FrmResponse::Checkout(_) | FrmResponse::Transaction(_) => { + Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Pre(Sale) flow response in current post flow".to_string(), + )), + }) + } + }; + + if frm_suggestion == Some(FrmSuggestion::FrmAutoRefund) { + payment_data.payment_attempt = db + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + PaymentAttemptUpdate::RejectUpdate { + status: AttemptStatus::Failure, + error_code: Some(Some(frm_data.fraud_check.frm_status.to_string())), + error_message: Some(Some(REFUND_INITIATED.to_string())), + updated_by: frm_data.merchant_account.storage_scheme.to_string(), // merchant_decision: Some(MerchantDecision::AutoRefunded), + }, + frm_data.merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + payment_data.payment_intent = db + .update_payment_intent( + payment_data.payment_intent.clone(), + PaymentIntentUpdate::RejectUpdate { + status: IntentStatus::Failed, + merchant_decision: Some(MerchantDecision::AutoRefunded.to_string()), + updated_by: frm_data.merchant_account.storage_scheme.to_string(), + }, + frm_data.merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + frm_data.fraud_check = match frm_check_update { + Some(fraud_check_update) => db + .update_fraud_check_response_with_attempt_id( + frm_data.fraud_check.clone(), + fraud_check_update, + ) + .await + .map_err(|error| error.change_context(errors::ApiErrorResponse::PaymentNotFound))?, + None => frm_data.fraud_check.clone(), + }; + + Ok(frm_data) + } +} diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs new file mode 100644 index 000000000000..b92df3d3ef9f --- /dev/null +++ b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs @@ -0,0 +1,346 @@ +use async_trait::async_trait; +use common_enums::FrmSuggestion; +use common_utils::ext_traits::Encode; +use diesel_models::enums::FraudCheckLastStep; +use router_env::{instrument, tracing}; +use uuid::Uuid; + +use super::{Domain, FraudCheckOperation, GetTracker, UpdateTracker}; +use crate::{ + core::{ + errors::RouterResult, + fraud_check::{ + self as frm_core, + types::{FrmData, PaymentDetails, PaymentToFrmData}, + ConnectorDetailsCore, + }, + payments, + }, + db::StorageInterface, + errors, + types::{ + api::fraud_check as frm_api, + domain, + fraud_check::{ + FraudCheckCheckoutData, FraudCheckResponseData, FraudCheckTransactionData, FrmRequest, + FrmResponse, FrmRouterData, + }, + storage::{ + enums::{FraudCheckStatus, FraudCheckType}, + fraud_check::{FraudCheckNew, FraudCheckUpdate}, + }, + ResponseId, + }, + AppState, +}; + +#[derive(Debug, Clone, Copy)] +pub struct FraudCheckPre; + +impl FraudCheckOperation for &FraudCheckPre { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(*self) + } +} + +impl FraudCheckOperation for FraudCheckPre { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(self) + } +} + +#[async_trait] +impl GetTracker for FraudCheckPre { + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_data: PaymentToFrmData, + frm_connector_details: ConnectorDetailsCore, + ) -> RouterResult> { + let db = &*state.store; + + let payment_details: Option = + Encode::::encode_to_value(&PaymentDetails::from(payment_data.clone())) + .ok(); + + let existing_fraud_check = db + .find_fraud_check_by_payment_id_if_present( + payment_data.payment_intent.payment_id.clone(), + payment_data.merchant_account.merchant_id.clone(), + ) + .await + .ok(); + + let fraud_check = match existing_fraud_check { + Some(Some(fraud_check)) => Ok(fraud_check), + _ => { + db.insert_fraud_check_response(FraudCheckNew { + frm_id: Uuid::new_v4().simple().to_string(), + payment_id: payment_data.payment_intent.payment_id.clone(), + merchant_id: payment_data.merchant_account.merchant_id.clone(), + attempt_id: payment_data.payment_attempt.attempt_id.clone(), + created_at: common_utils::date_time::now(), + frm_name: frm_connector_details.connector_name, + frm_transaction_id: None, + frm_transaction_type: FraudCheckType::PreFrm, + frm_status: FraudCheckStatus::Pending, + frm_score: None, + frm_reason: None, + frm_error: None, + payment_details, + metadata: None, + modified_at: common_utils::date_time::now(), + last_step: FraudCheckLastStep::Processing, + }) + .await + } + }; + + match fraud_check { + Ok(fraud_check_value) => { + let frm_data = FrmData { + payment_intent: payment_data.payment_intent, + payment_attempt: payment_data.payment_attempt, + merchant_account: payment_data.merchant_account, + address: payment_data.address, + fraud_check: fraud_check_value, + connector_details: payment_data.connector_details, + order_details: payment_data.order_details, + refund: None, + frm_metadata: payment_data.frm_metadata, + }; + Ok(Some(frm_data)) + } + Err(error) => { + router_env::logger::error!("inserting into fraud_check table failed {error:?}"); + Ok(None) + } + } + } +} + +#[async_trait] +impl Domain for FraudCheckPre { + #[instrument(skip_all)] + async fn post_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult> { + let router_data = frm_core::call_frm_service::( + state, + payment_data, + &mut frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::TransactionOrRecordRefund; + Ok(Some(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Transaction(FraudCheckTransactionData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + currency: router_data.request.currency, + payment_method: Some(router_data.payment_method), + error_code: router_data.request.error_code, + error_message: router_data.request.error_message, + connector_transaction_id: router_data.request.connector_transaction_id, + }), + response: FrmResponse::Transaction(router_data.response), + })) + } + + async fn pre_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult { + let router_data = frm_core::call_frm_service::( + state, + payment_data, + &mut frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::CheckoutOrSale; + Ok(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Checkout(FraudCheckCheckoutData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + currency: router_data.request.currency, + browser_info: router_data.request.browser_info, + payment_method_data: router_data.request.payment_method_data, + email: router_data.request.email, + gateway: router_data.request.gateway, + }), + response: FrmResponse::Checkout(router_data.response), + }) + } +} + +#[async_trait] +impl UpdateTracker for FraudCheckPre { + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + mut frm_data: FrmData, + payment_data: &mut payments::PaymentData, + _frm_suggestion: Option, + frm_router_data: FrmRouterData, + ) -> RouterResult { + let frm_check_update = match frm_router_data.response { + FrmResponse::Checkout(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + FraudCheckResponseData::RecordReturnResponse { + resource_id: _, + connector_metadata: _, + return_id: _, + } => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Record Return Response response in current Checkout flow" + .to_string(), + )), + }), + }, + }, + FrmResponse::Transaction(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let frm_status = payment_data + .frm_message + .as_ref() + .map_or(status, |frm_data| frm_data.frm_status); + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + FraudCheckResponseData::RecordReturnResponse { + resource_id: _, + connector_metadata: _, + return_id: _, + } => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Record Return Response response in current Checkout flow" + .to_string(), + )), + }), + }, + }, + FrmResponse::Sale(_response) + | FrmResponse::Fulfillment(_response) + | FrmResponse::RecordReturn(_response) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Pre(Sale) flow response in current post flow".to_string(), + )), + }), + }; + + frm_data.fraud_check = match frm_check_update { + Some(fraud_check_update) => db + .update_fraud_check_response_with_attempt_id( + frm_data.clone().fraud_check, + fraud_check_update, + ) + .await + .map_err(|error| error.change_context(errors::ApiErrorResponse::PaymentNotFound))?, + None => frm_data.clone().fraud_check, + }; + + Ok(frm_data) + } +} diff --git a/crates/router/src/core/fraud_check/types.rs b/crates/router/src/core/fraud_check/types.rs new file mode 100644 index 000000000000..e60458646f3e --- /dev/null +++ b/crates/router/src/core/fraud_check/types.rs @@ -0,0 +1,218 @@ +use api_models::{ + enums as api_enums, + enums::{PaymentMethod, PaymentMethodType}, + payments::Amount, + refunds::RefundResponse, +}; +use common_enums::FrmSuggestion; +use common_utils::pii::Email; +use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; +use masking::Serialize; +use serde::Deserialize; +use utoipa::ToSchema; + +use super::operation::BoxedFraudCheckOperation; +use crate::{ + pii::Secret, + types::{ + domain::MerchantAccount, + storage::{enums as storage_enums, fraud_check::FraudCheck}, + PaymentAddress, + }, +}; + +#[derive(Clone, Default, Debug)] +pub struct PaymentIntentCore { + pub payment_id: String, +} + +#[derive(Clone, Debug)] +pub struct PaymentAttemptCore { + pub attempt_id: String, + pub payment_details: Option, + pub amount: Amount, +} + +#[derive(Clone, Debug, Serialize)] +pub struct PaymentDetails { + pub amount: i64, + pub currency: Option, + pub payment_method: Option, + pub payment_method_type: Option, + pub refund_transaction_id: Option, +} +#[derive(Clone, Default, Debug)] +pub struct FrmMerchantAccount { + pub merchant_id: String, +} + +#[derive(Clone, Debug)] +pub struct FrmData { + pub payment_intent: PaymentIntent, + pub payment_attempt: PaymentAttempt, + pub merchant_account: MerchantAccount, + pub fraud_check: FraudCheck, + pub address: PaymentAddress, + pub connector_details: ConnectorDetailsCore, + pub order_details: Option>, + pub refund: Option, + pub frm_metadata: Option, +} + +#[derive(Debug)] +pub struct FrmInfo { + pub fraud_check_operation: BoxedFraudCheckOperation, + pub frm_data: Option, + pub suggested_action: Option, +} + +#[derive(Clone, Debug)] +pub struct ConnectorDetailsCore { + pub connector_name: String, + pub profile_id: String, +} +#[derive(Clone)] +pub struct PaymentToFrmData { + pub amount: Amount, + pub payment_intent: PaymentIntent, + pub payment_attempt: PaymentAttempt, + pub merchant_account: MerchantAccount, + pub address: PaymentAddress, + pub connector_details: ConnectorDetailsCore, + pub order_details: Option>, + pub frm_metadata: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FrmConfigsObject { + pub frm_enabled_pm: Option, + pub frm_enabled_pm_type: Option, + pub frm_enabled_gateway: Option, + pub frm_action: api_enums::FrmAction, + pub frm_preferred_flow_type: api_enums::FrmPreferredFlowTypes, +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFulfillmentSignifydApiRequest { + ///unique order_id for the order_details in the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///denotes the status of the fulfillment... can be one of PARTIAL, COMPLETE, REPLACEMENT, CANCELED + #[schema(value_type = Option, example = "COMPLETE")] + pub fulfillment_status: Option, + ///contains details of the fulfillment + #[schema(value_type = Vec)] + pub fulfillments: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct FrmFulfillmentRequest { + ///unique payment_id for the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub payment_id: String, + ///unique order_id for the order_details in the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///denotes the status of the fulfillment... can be one of PARTIAL, COMPLETE, REPLACEMENT, CANCELED + #[schema(value_type = Option, example = "COMPLETE")] + pub fulfillment_status: Option, + ///contains details of the fulfillment + #[schema(value_type = Vec)] + pub fulfillments: Vec, + //name of the tracking Company + #[schema(max_length = 255, example = "fedex")] + pub tracking_company: Option, + //tracking ID of the product + #[schema(max_length = 255, example = "track_8327446667")] + pub tracking_number: Option, + //tracking_url for tracking the product + pub tracking_url: Option, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Fulfillments { + ///shipment_id of the shipped items + #[schema(max_length = 255, example = "ship_101")] + pub shipment_id: String, + ///products sent in the shipment + #[schema(value_type = Option>)] + pub products: Option>, + ///destination address of the shipment + #[schema(value_type = Destination)] + pub destination: Destination, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde(untagged)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub enum FulfillmentStatus { + PARTIAL, + COMPLETE, + REPLACEMENT, + CANCELED, +} + +#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Product { + pub item_name: String, + pub item_quantity: i64, + pub item_id: String, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Destination { + pub full_name: Secret, + pub organization: Option, + pub email: Option, + pub address: Address, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Address { + pub street_address: Secret, + pub unit: Option>, + pub postal_code: Secret, + pub city: String, + pub province_code: Secret, + pub country_code: common_enums::CountryAlpha2, +} + +#[derive(Debug, ToSchema, Clone, Serialize)] +pub struct FrmFulfillmentResponse { + ///unique order_id for the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///shipment_ids used in the fulfillment overall...also data from previous fulfillments for the same transactions/order is sent + #[schema(example = r#"["ship_101", "ship_102"]"#)] + pub shipment_ids: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFulfillmentSignifydApiResponse { + ///unique order_id for the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///shipment_ids used in the fulfillment overall...also data from previous fulfillments for the same transactions/order is sent + #[schema(example = r#"["ship_101","ship_102"]"#)] + pub shipment_ids: Vec, +} + +pub const REFUND_INITIATED: &str = "Refund Initiated with the processor"; diff --git a/crates/router/src/core/gsm.rs b/crates/router/src/core/gsm.rs index ed72275a73ab..611a35d63632 100644 --- a/crates/router/src/core/gsm.rs +++ b/crates/router/src/core/gsm.rs @@ -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 diff --git a/crates/router/src/core/health_check.rs b/crates/router/src/core/health_check.rs new file mode 100644 index 000000000000..bc523b4fba66 --- /dev/null +++ b/crates/router/src/core/health_check.rs @@ -0,0 +1,130 @@ +#[cfg(feature = "olap")] +use analytics::health_check::HealthCheck; +use error_stack::ResultExt; +use router_env::logger; + +use crate::{ + consts, + core::errors::{self, CustomResult}, + routes::app, + services::api as services, +}; + +#[async_trait::async_trait] +pub trait HealthCheckInterface { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError>; + async fn health_check_redis(&self) -> CustomResult<(), errors::HealthCheckRedisError>; + async fn health_check_locker(&self) -> CustomResult<(), errors::HealthCheckLockerError>; + async fn health_check_outgoing(&self) -> CustomResult<(), errors::HealthCheckOutGoing>; + #[cfg(feature = "olap")] + async fn health_check_analytics(&self) -> CustomResult<(), errors::HealthCheckDBError>; +} + +#[async_trait::async_trait] +impl HealthCheckInterface for app::AppState { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { + let db = &*self.store; + db.health_check_db().await?; + Ok(()) + } + + async fn health_check_redis(&self) -> CustomResult<(), errors::HealthCheckRedisError> { + let db = &*self.store; + let redis_conn = db + .get_redis_conn() + .change_context(errors::HealthCheckRedisError::RedisConnectionError)?; + + redis_conn + .serialize_and_set_key_with_expiry("test_key", "test_value", 30) + .await + .change_context(errors::HealthCheckRedisError::SetFailed)?; + + logger::debug!("Redis set_key was successful"); + + redis_conn + .get_key("test_key") + .await + .change_context(errors::HealthCheckRedisError::GetFailed)?; + + logger::debug!("Redis get_key was successful"); + + redis_conn + .delete_key("test_key") + .await + .change_context(errors::HealthCheckRedisError::DeleteFailed)?; + + logger::debug!("Redis delete_key was successful"); + + Ok(()) + } + + async fn health_check_locker(&self) -> CustomResult<(), errors::HealthCheckLockerError> { + let locker = &self.conf.locker; + if !locker.mock_locker { + let mut url = locker.host_rs.to_owned(); + url.push_str(consts::LOCKER_HEALTH_CALL_PATH); + let request = services::Request::new(services::Method::Get, &url); + services::call_connector_api(self, request) + .await + .change_context(errors::HealthCheckLockerError::FailedToCallLocker)? + .ok(); + } + + logger::debug!("Locker call was successful"); + + Ok(()) + } + + #[cfg(feature = "olap")] + async fn health_check_analytics(&self) -> CustomResult<(), errors::HealthCheckDBError> { + let analytics = &self.pool; + match analytics { + analytics::AnalyticsProvider::Sqlx(client) => client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::SqlxAnalyticsError), + analytics::AnalyticsProvider::Clickhouse(client) => client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::ClickhouseAnalyticsError), + analytics::AnalyticsProvider::CombinedCkh(sqlx_client, ckh_client) => { + sqlx_client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::SqlxAnalyticsError)?; + ckh_client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::ClickhouseAnalyticsError) + } + analytics::AnalyticsProvider::CombinedSqlx(sqlx_client, ckh_client) => { + sqlx_client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::SqlxAnalyticsError)?; + ckh_client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::ClickhouseAnalyticsError) + } + } + } + + async fn health_check_outgoing(&self) -> CustomResult<(), errors::HealthCheckOutGoing> { + let request = services::Request::new(services::Method::Get, consts::OUTGOING_CALL_URL); + services::call_connector_api(self, request) + .await + .map_err(|err| errors::HealthCheckOutGoing::OutGoingFailed { + message: err.to_string(), + })? + .map_err(|err| errors::HealthCheckOutGoing::OutGoingFailed { + message: format!( + "Got a non 200 status while making outgoing request. Error {:?}", + err.response + ), + })?; + + logger::debug!("Outgoing request successful"); + Ok(()) + } +} diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index aa82b4a3a636..4bd2555792a2 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -1,6 +1,6 @@ use api_models::{enums as api_enums, locker_migration::MigrateCardResponse}; use common_utils::errors::CustomResult; -use diesel_models::PaymentMethod; +use diesel_models::{enums as storage_enums, PaymentMethod}; use error_stack::{FutureExt, ResultExt}; use futures::TryFutureExt; @@ -79,10 +79,21 @@ pub async fn call_to_locker( ) -> CustomResult { let mut cards_moved = 0; - for pm in payment_methods { + 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?; + .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, @@ -90,6 +101,10 @@ pub async fn call_to_locker( card_exp_year: card.card_exp_year, card_holder_name: card.name_on_card, nick_name: card.nick_name.map(masking::Secret::new), + card_issuing_country: None, + card_network: None, + card_issuer: None, + card_type: None, }; let pm_create = api::PaymentMethodCreate { @@ -98,33 +113,42 @@ pub async fn call_to_locker( payment_method_issuer: pm.payment_method_issuer, payment_method_issuer_code: pm.payment_method_issuer_code, card: Some(card_details.clone()), + bank_transfer: None, metadata: pm.metadata, customer_id: Some(pm.customer_id), card_network: card.card_brand, }; - let (_add_card_rs_resp, _is_duplicate) = 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_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 - ); + "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/mandate.rs b/crates/router/src/core/mandate.rs index 55de11549a4b..7299ef624d87 100644 --- a/crates/router/src/core/mandate.rs +++ b/crates/router/src/core/mandate.rs @@ -1,13 +1,18 @@ +pub mod helpers; +pub mod utils; use api_models::payments; use common_utils::{ext_traits::Encode, pii}; -use diesel_models::enums as storage_enums; -use error_stack::{report, ResultExt}; +use diesel_models::{enums as storage_enums, Mandate}; +use error_stack::{report, IntoReport, ResultExt}; use futures::future; use router_env::{instrument, logger, tracing}; -use super::payments::helpers; +use super::payments::helpers as payment_helper; use crate::{ - core::errors::{self, RouterResponse, StorageErrorExt}, + core::{ + errors::{self, RouterResponse, StorageErrorExt}, + payments::CallConnectorAction, + }, db::StorageInterface, routes::{metrics, AppState}, services, @@ -16,6 +21,7 @@ use crate::{ api::{ customers, mandates::{self, MandateResponseExt}, + ConnectorData, GetToken, }, domain, storage, transformers::ForeignTryFrom, @@ -27,6 +33,7 @@ use crate::{ pub async fn get_mandate( state: AppState, merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, req: mandates::MandateId, ) -> RouterResponse { let mandate = state @@ -36,7 +43,7 @@ pub async fn get_mandate( .await .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; Ok(services::ApplicationResponse::Json( - mandates::MandateResponse::from_db_mandate(&state, mandate).await?, + mandates::MandateResponse::from_db_mandate(&state, key_store, mandate).await?, )) } @@ -44,26 +51,104 @@ pub async fn get_mandate( pub async fn revoke_mandate( state: AppState, merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, req: mandates::MandateId, ) -> RouterResponse { let db = state.store.as_ref(); let mandate = db - .update_mandate_by_merchant_id_mandate_id( - &merchant_account.merchant_id, - &req.mandate_id, - storage::MandateUpdate::StatusUpdate { - mandate_status: storage::enums::MandateStatus::Revoked, - }, - ) + .find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, &req.mandate_id) .await .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; - Ok(services::ApplicationResponse::Json( - mandates::MandateRevokedResponse { - mandate_id: mandate.mandate_id, - status: mandate.mandate_status, - }, - )) + let mandate_revoke_status = match mandate.mandate_status { + common_enums::MandateStatus::Active + | common_enums::MandateStatus::Inactive + | common_enums::MandateStatus::Pending => { + let profile_id = + helpers::get_profile_id_for_mandate(&state, &merchant_account, mandate.clone()) + .await?; + + let merchant_connector_account = payment_helper::get_merchant_connector_account( + &state, + &merchant_account.merchant_id, + None, + &key_store, + &profile_id, + &mandate.connector.clone(), + mandate.merchant_connector_id.as_ref(), + ) + .await?; + + let connector_data = ConnectorData::get_connector_by_name( + &state.conf.connectors, + &mandate.connector, + GetToken::Connector, + mandate.merchant_connector_id.clone(), + )?; + let connector_integration: services::BoxedConnectorIntegration< + '_, + types::api::MandateRevoke, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + > = connector_data.connector.get_connector_integration(); + + let router_data = utils::construct_mandate_revoke_router_data( + merchant_connector_account, + &merchant_account, + mandate.clone(), + ) + .await?; + + let response = services::execute_connector_processing_step( + &state, + connector_integration, + &router_data, + CallConnectorAction::Trigger, + None, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + match response.response { + Ok(_) => { + let update_mandate = db + .update_mandate_by_merchant_id_mandate_id( + &merchant_account.merchant_id, + &req.mandate_id, + storage::MandateUpdate::StatusUpdate { + mandate_status: storage::enums::MandateStatus::Revoked, + }, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; + Ok(services::ApplicationResponse::Json( + mandates::MandateRevokedResponse { + mandate_id: update_mandate.mandate_id, + status: update_mandate.mandate_status, + error_code: None, + error_message: None, + }, + )) + } + + Err(err) => Err(errors::ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: mandate.connector, + status_code: err.status_code, + reason: err.reason, + }) + .into_report(), + } + } + common_enums::MandateStatus::Revoked => { + Err(errors::ApiErrorResponse::MandateValidationFailed { + reason: "Mandate has already been revoked".to_string(), + }) + .into_report() + } + }; + mandate_revoke_status } #[instrument(skip(db))] @@ -101,6 +186,7 @@ pub async fn update_connector_mandate_id( pub async fn get_customer_mandates( state: AppState, merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, req: customers::CustomerId, ) -> RouterResponse> { let mandates = state @@ -120,7 +206,10 @@ pub async fn get_customer_mandates( } else { let mut response_vec = Vec::with_capacity(mandates.len()); for mandate in mandates { - response_vec.push(mandates::MandateResponse::from_db_mandate(&state, mandate).await?); + response_vec.push( + mandates::MandateResponse::from_db_mandate(&state, key_store.clone(), mandate) + .await?, + ); } Ok(services::ApplicationResponse::Json(response_vec)) } @@ -137,7 +226,72 @@ where _ => Some(router_data.request.get_payment_method_data()), } } +pub async fn update_mandate_procedure( + state: &AppState, + resp: types::RouterData, + mandate: Mandate, + merchant_id: &str, + pm_id: Option, +) -> errors::RouterResult> +where + FData: MandateBehaviour, +{ + let mandate_details = match &resp.response { + Ok(types::PaymentsResponseData::TransactionResponse { + mandate_reference, .. + }) => mandate_reference, + Ok(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Unexpected response received")?, + Err(_) => return Ok(resp), + }; + + let old_record = payments::UpdateHistory { + connector_mandate_id: mandate.connector_mandate_id, + payment_method_id: mandate.payment_method_id, + original_payment_id: mandate.original_payment_id, + }; + let mandate_ref = mandate + .connector_mandate_ids + .parse_value::("Connector Reference Id") + .change_context(errors::ApiErrorResponse::MandateDeserializationFailed)?; + + let mut update_history = mandate_ref.update_history.unwrap_or_default(); + update_history.push(old_record); + + let updated_mandate_ref = payments::ConnectorMandateReferenceId { + connector_mandate_id: mandate_details + .as_ref() + .and_then(|mandate_ref| mandate_ref.connector_mandate_id.clone()), + payment_method_id: pm_id.clone(), + update_history: Some(update_history), + }; + + let connector_mandate_ids = + Encode::::encode_to_value(&updated_mandate_ref) + .change_context(errors::ApiErrorResponse::InternalServerError) + .map(masking::Secret::new)?; + + let _update_mandate_details = state + .store + .update_mandate_by_merchant_id_mandate_id( + merchant_id, + &mandate.mandate_id, + diesel_models::MandateUpdate::ConnectorMandateIdUpdate { + connector_mandate_id: mandate_details + .as_ref() + .and_then(|man_ref| man_ref.connector_mandate_id.clone()), + connector_mandate_ids: Some(connector_mandate_ids), + payment_method_id: pm_id + .unwrap_or("Error retrieving the payment_method_id".to_string()), + original_payment_id: Some(resp.payment_id.clone()), + }, + ) + .await + .change_context(errors::ApiErrorResponse::MandateUpdateFailed)?; + Ok(resp) +} pub async fn mandate_procedure( state: &AppState, mut resp: types::RouterData, @@ -218,7 +372,7 @@ where }) .transpose()?; - if let Some(new_mandate_data) = helpers::generate_mandate( + if let Some(new_mandate_data) = payment_helper::generate_mandate( resp.merchant_id.clone(), resp.payment_id.clone(), resp.connector.clone(), @@ -257,6 +411,8 @@ where api_models::payments::ConnectorMandateReferenceId { connector_mandate_id: connector_id.connector_mandate_id, payment_method_id: connector_id.payment_method_id, + update_history:None, + } ))) })); @@ -282,6 +438,7 @@ where pub async fn retrieve_mandates_list( state: AppState, merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, constraints: api_models::mandates::MandateListConstraints, ) -> RouterResponse> { let mandates = state @@ -291,11 +448,9 @@ pub async fn retrieve_mandates_list( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to retrieve mandates")?; - let mandates_list = future::try_join_all( - mandates - .into_iter() - .map(|mandate| mandates::MandateResponse::from_db_mandate(&state, mandate)), - ) + let mandates_list = future::try_join_all(mandates.into_iter().map(|mandate| { + mandates::MandateResponse::from_db_mandate(&state, key_store.clone(), mandate) + })) .await?; Ok(services::ApplicationResponse::Json(mandates_list)) } diff --git a/crates/router/src/core/mandate/helpers.rs b/crates/router/src/core/mandate/helpers.rs new file mode 100644 index 000000000000..150130ed9e5d --- /dev/null +++ b/crates/router/src/core/mandate/helpers.rs @@ -0,0 +1,35 @@ +use common_utils::errors::CustomResult; +use diesel_models::Mandate; +use error_stack::ResultExt; + +use crate::{core::errors, routes::AppState, types::domain}; + +pub async fn get_profile_id_for_mandate( + state: &AppState, + merchant_account: &domain::MerchantAccount, + mandate: Mandate, +) -> CustomResult { + let profile_id = if let Some(ref payment_id) = mandate.original_payment_id { + let pi = state + .store + .find_payment_intent_by_payment_id_merchant_id( + payment_id, + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + let profile_id = + pi.profile_id + .clone() + .ok_or(errors::ApiErrorResponse::BusinessProfileNotFound { + id: pi + .profile_id + .unwrap_or_else(|| "Profile id is Null".to_string()), + })?; + Ok(profile_id) + } else { + Err(errors::ApiErrorResponse::PaymentNotFound) + }?; + Ok(profile_id) +} diff --git a/crates/router/src/core/mandate/utils.rs b/crates/router/src/core/mandate/utils.rs new file mode 100644 index 000000000000..4863edd6fcba --- /dev/null +++ b/crates/router/src/core/mandate/utils.rs @@ -0,0 +1,78 @@ +use std::marker::PhantomData; + +use common_utils::{errors::CustomResult, ext_traits::ValueExt}; +use diesel_models::Mandate; +use error_stack::ResultExt; + +use crate::{ + core::{errors, payments::helpers}, + types::{self, domain, PaymentAddress}, +}; +const IRRELEVANT_PAYMENT_ID_IN_MANDATE_REVOKE_FLOW: &str = + "irrelevant_payment_id_in_mandate_revoke_flow"; + +const IRRELEVANT_ATTEMPT_ID_IN_MANDATE_REVOKE_FLOW: &str = + "irrelevant_attempt_id_in_mandate_revoke_flow"; + +const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_MANDATE_REVOKE_FLOW: &str = + "irrelevant_connector_request_reference_id_in_mandate_revoke_flow"; + +pub async fn construct_mandate_revoke_router_data( + merchant_connector_account: helpers::MerchantConnectorAccountType, + merchant_account: &domain::MerchantAccount, + mandate: Mandate, +) -> CustomResult { + let auth_type: types::ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let router_data = types::RouterData { + flow: PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id: Some(mandate.customer_id), + connector_customer: None, + connector: mandate.connector, + payment_id: mandate + .original_payment_id + .unwrap_or_else(|| IRRELEVANT_PAYMENT_ID_IN_MANDATE_REVOKE_FLOW.to_string()), + attempt_id: IRRELEVANT_ATTEMPT_ID_IN_MANDATE_REVOKE_FLOW.to_string(), + status: diesel_models::enums::AttemptStatus::default(), + payment_method: diesel_models::enums::PaymentMethod::default(), + connector_auth_type: auth_type, + description: None, + return_url: None, + address: PaymentAddress::default(), + auth_type: diesel_models::enums::AuthenticationType::default(), + connector_meta_data: None, + amount_captured: None, + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + recurring_mandate_payment_data: None, + preprocessing_id: None, + payment_method_balance: None, + connector_api_version: None, + request: types::MandateRevokeRequestData { + mandate_id: mandate.mandate_id, + connector_mandate_id: mandate.connector_mandate_id, + }, + response: Err(types::ErrorResponse::get_not_implemented()), + payment_method_id: None, + connector_request_reference_id: + IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_MANDATE_REVOKE_FLOW.to_string(), + test_mode: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + refund_id: None, + dispute_id: None, + }; + + Ok(router_data) +} 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..5af948bcf3e0 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,21 +1,25 @@ -use api_models::admin as admin_types; +use api_models::{admin as admin_types, payments::PaymentLinkStatusWrap}; use common_utils::{ consts::{ - DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_THEME, + DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, + DEFAULT_SESSION_EXPIRY, }, - 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::{ - core::payments::helpers, 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( @@ -23,13 +27,22 @@ pub async fn retrieve_payment_link( payment_link_id: String, ) -> RouterResponse { let db = &*state.store; - let payment_link_object = db + let payment_link_config = db .find_payment_link_by_payment_link_id(&payment_link_id) .await .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; - let response = - api_models::payments::RetrievePaymentLinkResponse::foreign_from(payment_link_object); + let session_expiry = payment_link_config.fulfilment_time.unwrap_or_else(|| { + common_utils::date_time::now() + .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY)) + }); + + let status = check_payment_link_status(session_expiry); + + let response = api_models::payments::RetrievePaymentLinkResponse::foreign_from(( + payment_link_config, + status, + )); Ok(services::ApplicationResponse::Json(response)) } @@ -54,40 +67,46 @@ pub async fn intiate_payment_link_flow( .get_required_value("payment_link_id") .change_context(errors::ApiErrorResponse::PaymentLinkNotFound)?; - helpers::validate_payment_status_against_not_allowed_statuses( - &payment_intent.status, - &[ - storage_enums::IntentStatus::Cancelled, - storage_enums::IntentStatus::Succeeded, - storage_enums::IntentStatus::Processing, - storage_enums::IntentStatus::RequiresCapture, - storage_enums::IntentStatus::RequiresMerchantAction, - ], - "create payment link", - )?; + let merchant_name_from_merchant_account = merchant_account + .merchant_name + .clone() + .map(|merchant_name| merchant_name.into_inner().peek().to_owned()) + .unwrap_or_default(); let payment_link = db .find_payment_link_by_payment_link_id(&payment_link_id) .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_value) = payment_link.payment_link_config { + extract_payment_link_config(pl_config_value)? + } else { + admin_types::PaymentLinkConfig { + theme: DEFAULT_BACKGROUND_COLOR.to_string(), + logo: DEFAULT_MERCHANT_LOGO.to_string(), + seller_name: merchant_name_from_merchant_account, + sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(), + } + }; - let order_details = validate_order_details(payment_intent.order_details)?; + let profile_id = payment_link + .profile_id + .or(payment_intent.profile_id) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Profile id missing in payment link and 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 return_url = if let Some(payment_create_return_url) = payment_intent.return_url { + let return_url = if let Some(payment_create_return_url) = payment_intent.return_url.clone() { payment_create_return_url } else { - merchant_account + business_profile .return_url .ok_or(errors::ApiErrorResponse::MissingRequiredField { field_name: "return_url", @@ -97,55 +116,117 @@ pub async fn intiate_payment_link_flow( let (pub_key, currency, client_secret) = validate_sdk_requirements( merchant_account.publishable_key, payment_intent.currency, - payment_intent.client_secret, + payment_intent.client_secret.clone(), )?; + let amount = currency + .to_currency_base_unit(payment_intent.amount) + .into_report() + .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; + let order_details = validate_order_details(payment_intent.order_details.clone(), currency)?; + + let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| { + payment_intent + .created_at + .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY)) + }); - let (default_sdk_theme, default_background_color) = - (DEFAULT_SDK_THEME, DEFAULT_BACKGROUND_COLOR); + // converting first letter of merchant name to upperCase + let merchant_name = capitalize_first_char(&payment_link_config.seller_name); + let css_script = get_color_scheme_css(payment_link_config.clone()); + let payment_link_status = check_payment_link_status(session_expiry); + + let is_terminal_state = check_payment_link_invalid_conditions( + &payment_intent.status, + &[ + storage_enums::IntentStatus::Cancelled, + storage_enums::IntentStatus::Failed, + storage_enums::IntentStatus::Processing, + storage_enums::IntentStatus::RequiresCapture, + storage_enums::IntentStatus::RequiresMerchantAction, + storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::PartiallyCaptured, + ], + ); + if is_terminal_state || payment_link_status == api_models::payments::PaymentLinkStatus::Expired + { + let status = match payment_link_status { + api_models::payments::PaymentLinkStatus::Active => { + PaymentLinkStatusWrap::IntentStatus(payment_intent.status) + } + api_models::payments::PaymentLinkStatus::Expired => { + if is_terminal_state { + PaymentLinkStatusWrap::IntentStatus(payment_intent.status) + } else { + PaymentLinkStatusWrap::PaymentLinkStatus( + api_models::payments::PaymentLinkStatus::Expired, + ) + } + } + }; + + let attempt_id = payment_intent.active_attempt.get_id().clone(); + let payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_intent.payment_id, + &merchant_id, + &attempt_id.clone(), + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let payment_details = api_models::payments::PaymentLinkStatusDetails { + amount, + currency, + payment_id: payment_intent.payment_id, + merchant_name, + merchant_logo: payment_link_config.logo.clone(), + created: payment_link.created_at, + status, + error_code: payment_attempt.error_code, + error_message: payment_attempt.error_message, + redirect: false, + theme: payment_link_config.theme.clone(), + return_url: return_url.clone(), + }; + let js_script = get_js_script( + api_models::payments::PaymentLinkData::PaymentLinkStatusDetails(payment_details), + )?; + let payment_link_error_data = services::PaymentLinkStatusData { + js_script, + css_script, + }; + return Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new( + services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_error_data), + ))); + }; let payment_details = api_models::payments::PaymentLinkDetails { - amount: payment_intent.amount, + amount, currency, payment_id: payment_intent.payment_id, - merchant_name: payment_link.custom_merchant_name.unwrap_or( - merchant_account - .merchant_name - .map(|merchant_name| merchant_name.into_inner().peek().to_owned()) - .unwrap_or_default(), - ), + merchant_name, order_details, return_url, - expiry: payment_link.fulfilment_time, + session_expiry, pub_key, client_secret, - merchant_logo: payment_link_config - .clone() - .map(|pl_config| { - pl_config - .merchant_logo - .unwrap_or(DEFAULT_MERCHANT_LOGO.to_string()) - }) - .unwrap_or_default(), + merchant_logo: payment_link_config.logo.clone(), max_items_visible_after_collapse: 3, - sdk_theme: payment_link_config.clone().and_then(|pl_config| { - pl_config - .color_scheme - .map(|color| color.sdk_theme.unwrap_or(default_sdk_theme.to_string())) - }), + theme: payment_link_config.theme.clone(), + merchant_description: payment_intent.description, + sdk_layout: payment_link_config.sdk_layout.clone(), }; - let js_script = get_js_script(payment_details)?; - let css_script = get_color_scheme_css( - payment_link_config.clone(), - default_background_color.to_string(), - ); + let js_script = get_js_script(api_models::payments::PaymentLinkData::PaymentLinkDetails( + payment_details, + ))?; let payment_link_data = services::PaymentLinkFormData { js_script, sdk_url: state.conf.payment_link.sdk_url.clone(), css_script, }; Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new( - payment_link_data, + services::api::PaymentLinkAction::PaymentLinkFormData(payment_link_data), ))) } @@ -153,30 +234,16 @@ pub async fn intiate_payment_link_flow( The get_js_script function is used to inject dynamic value to payment_link sdk, which is unique to every payment. */ -fn get_js_script( - payment_details: api_models::payments::PaymentLinkDetails, -) -> RouterResult { +fn get_js_script(payment_details: api_models::payments::PaymentLinkData) -> RouterResult { let payment_details_str = serde_json::to_string(&payment_details) .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to serialize PaymentLinkDetails")?; + .attach_printable("Failed to serialize PaymentLinkData")?; Ok(format!("window.__PAYMENT_DETAILS = {payment_details_str};")) } -fn get_color_scheme_css( - payment_link_config: Option, - default_primary_color: String, -) -> String { - let background_primary_color = payment_link_config - .and_then(|pl_config| { - pl_config.color_scheme.map(|color| { - color - .background_primary_color - .unwrap_or(default_primary_color.clone()) - }) - }) - .unwrap_or(default_primary_color); - +fn get_color_scheme_css(payment_link_config: api_models::admin::PaymentLinkConfig) -> String { + let background_primary_color = payment_link_config.theme; format!( ":root {{ --primary-color: {background_primary_color}; @@ -203,10 +270,41 @@ 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( + payment_link_expiry: PrimitiveDateTime, +) -> api_models::payments::PaymentLinkStatus { + let curr_time = common_utils::date_time::now(); + + if curr_time > payment_link_expiry { + api_models::payments::PaymentLinkStatus::Expired + } else { + api_models::payments::PaymentLinkStatus::Active + } +} + fn validate_order_details( order_details: Option>>, + currency: api_models::enums::Currency, ) -> Result< - Option>, + Option>, error_stack::Report, > { let order_details = order_details @@ -225,13 +323,258 @@ fn validate_order_details( }) .transpose()?; - let updated_order_details = order_details.map(|mut order_details| { - for order in order_details.iter_mut() { - if order.product_img_link.is_none() { - order.product_img_link = Some(DEFAULT_PRODUCT_IMG.to_string()); + let updated_order_details = match order_details { + Some(mut order_details) => { + let mut order_details_amount_string_array: Vec< + api_models::payments::OrderDetailsWithStringAmount, + > = Vec::new(); + for order in order_details.iter_mut() { + let mut order_details_amount_string : api_models::payments::OrderDetailsWithStringAmount = Default::default(); + if order.product_img_link.is_none() { + order_details_amount_string.product_img_link = + Some(DEFAULT_PRODUCT_IMG.to_string()) + } else { + order_details_amount_string.product_img_link = order.product_img_link.clone() + }; + order_details_amount_string.amount = currency + .to_currency_base_unit(order.amount) + .into_report() + .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; + order_details_amount_string.product_name = + capitalize_first_char(&order.product_name.clone()); + order_details_amount_string.quantity = order.quantity; + order_details_amount_string_array.push(order_details_amount_string) } + Some(order_details_amount_string_array) } - order_details - }); + None => None, + }; Ok(updated_order_details) } + +pub fn extract_payment_link_config( + pl_config: serde_json::Value, +) -> Result> { + serde_json::from_value::(pl_config.clone()) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", + }) +} + +pub fn get_payment_link_config_based_on_priority( + payment_create_link_config: Option, + business_link_config: Option, + merchant_name: String, + default_domain_name: String, +) -> Result<(admin_types::PaymentLinkConfig, String), error_stack::Report> +{ + let (domain_name, business_config) = if let Some(business_config) = business_link_config { + let extracted_value: api_models::admin::BusinessPaymentLinkConfig = business_config + .parse_value("BusinessPaymentLinkConfig") + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", + }) + .attach_printable("Invalid payment_link_config given in business config")?; + + ( + extracted_value + .domain_name + .clone() + .map(|d_name| format!("https://{}", d_name)) + .unwrap_or_else(|| default_domain_name.clone()), + Some(extracted_value.config), + ) + } else { + (default_domain_name, None) + }; + + let theme = payment_create_link_config + .as_ref() + .and_then(|pc_config| pc_config.config.theme.clone()) + .or_else(|| { + business_config + .as_ref() + .and_then(|business_config| business_config.theme.clone()) + }) + .unwrap_or(DEFAULT_BACKGROUND_COLOR.to_string()); + + let logo = payment_create_link_config + .as_ref() + .and_then(|pc_config| pc_config.config.logo.clone()) + .or_else(|| { + business_config + .as_ref() + .and_then(|business_config| business_config.logo.clone()) + }) + .unwrap_or(DEFAULT_MERCHANT_LOGO.to_string()); + + let seller_name = payment_create_link_config + .as_ref() + .and_then(|pc_config| pc_config.config.seller_name.clone()) + .or_else(|| { + business_config + .as_ref() + .and_then(|business_config| business_config.seller_name.clone()) + }) + .unwrap_or(merchant_name.clone()); + + let sdk_layout = payment_create_link_config + .as_ref() + .and_then(|pc_config| pc_config.config.sdk_layout.clone()) + .or_else(|| { + business_config + .as_ref() + .and_then(|business_config| business_config.sdk_layout.clone()) + }) + .unwrap_or(DEFAULT_SDK_LAYOUT.to_owned()); + + let payment_link_config = admin_types::PaymentLinkConfig { + theme, + logo, + seller_name, + sdk_layout, + }; + + Ok((payment_link_config, domain_name)) +} + +fn capitalize_first_char(s: &str) -> String { + if let Some(first_char) = s.chars().next() { + let capitalized = first_char.to_uppercase(); + let mut result = capitalized.to_string(); + if let Some(remaining) = s.get(1..) { + result.push_str(remaining); + } + result + } else { + s.to_owned() + } +} + +fn check_payment_link_invalid_conditions( + intent_status: &storage_enums::IntentStatus, + not_allowed_statuses: &[storage_enums::IntentStatus], +) -> bool { + not_allowed_statuses.contains(intent_status) +} + +pub async fn get_payment_link_status( + state: AppState, + merchant_account: domain::MerchantAccount, + merchant_id: String, + payment_id: String, +) -> RouterResponse { + let db = &*state.store; + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id( + &payment_id, + &merchant_id, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let attempt_id = payment_intent.active_attempt.get_id().clone(); + let payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_intent.payment_id, + &merchant_id, + &attempt_id.clone(), + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let payment_link_id = payment_intent + .payment_link_id + .get_required_value("payment_link_id") + .change_context(errors::ApiErrorResponse::PaymentLinkNotFound)?; + + let merchant_name_from_merchant_account = merchant_account + .merchant_name + .clone() + .map(|merchant_name| merchant_name.into_inner().peek().to_owned()) + .unwrap_or_default(); + + let payment_link = db + .find_payment_link_by_payment_link_id(&payment_link_id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; + + let payment_link_config = if let Some(pl_config_value) = payment_link.payment_link_config { + extract_payment_link_config(pl_config_value)? + } else { + admin_types::PaymentLinkConfig { + theme: DEFAULT_BACKGROUND_COLOR.to_string(), + logo: DEFAULT_MERCHANT_LOGO.to_string(), + seller_name: merchant_name_from_merchant_account, + sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(), + } + }; + + let currency = + payment_intent + .currency + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "currency", + })?; + + let amount = currency + .to_currency_base_unit(payment_attempt.net_amount) + .into_report() + .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; + + // converting first letter of merchant name to upperCase + let merchant_name = capitalize_first_char(&payment_link_config.seller_name); + let css_script = get_color_scheme_css(payment_link_config.clone()); + + let profile_id = payment_link + .profile_id + .or(payment_intent.profile_id) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Profile id missing in payment link and 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 return_url = if let Some(payment_create_return_url) = payment_intent.return_url.clone() { + payment_create_return_url + } else { + business_profile + .return_url + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "return_url", + })? + }; + + let payment_details = api_models::payments::PaymentLinkStatusDetails { + amount, + currency, + payment_id: payment_intent.payment_id, + merchant_name, + merchant_logo: payment_link_config.logo.clone(), + created: payment_link.created_at, + status: PaymentLinkStatusWrap::IntentStatus(payment_intent.status), + error_code: payment_attempt.error_code, + error_message: payment_attempt.error_message, + redirect: true, + theme: payment_link_config.theme.clone(), + return_url, + }; + let js_script = get_js_script( + api_models::payments::PaymentLinkData::PaymentLinkStatusDetails(payment_details), + )?; + let payment_link_status_data = services::PaymentLinkStatusData { + js_script, + css_script, + }; + Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new( + services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_status_data), + ))) +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html deleted file mode 100644 index abacf0998f67..000000000000 --- a/crates/router/src/core/payment_link/payment_link.html +++ /dev/null @@ -1,1497 +0,0 @@ - - - - - - {{ hyperloader_sdk_link }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.css b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.css new file mode 100644 index 000000000000..ee5600e42bfe --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.css @@ -0,0 +1,785 @@ +{{ css_color_scheme }} + +html, +body { + height: 100%; + overflow: hidden; +} + +body { + display: flex; + flex-flow: column; + align-items: center; + justify-content: flex-start; + margin: 0; + color: #333333; +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.hide-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.hide-scrollbar { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ +} + +/* For ellipsis on text lines */ +.ellipsis-container-3 { + height: 4em; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + text-overflow: ellipsis; + white-space: normal; +} + +.hidden { + display: none !important; +} + +.hyper-checkout { + display: flex; + background-color: #f8f9fb; + color: #333333; + width: 100%; + height: 100%; + overflow: scroll; +} + +#hyper-footer { + width: 100vw; + display: flex; + justify-content: center; + padding: 20px 0; +} + +.main { + display: flex; + flex-flow: column; + justify-content: center; + align-items: center; + min-width: 600px; + width: 50vw; +} + +#hyper-checkout-details { + font-family: "Montserrat"; +} + +.hyper-checkout-payment { + min-width: 600px; + box-shadow: 0px 0px 5px #d1d1d1; + border-radius: 8px; + background-color: #fefefe; +} + +.hyper-checkout-payment-content-details { + display: flex; + flex-flow: column; + justify-content: space-between; + align-content: space-between; +} + +.content-details-wrap { + display: flex; + flex-flow: row; + margin: 20px 20px 30px 20px; + justify-content: space-between; +} + +.hyper-checkout-payment-price { + font-weight: 700; + font-size: 40px; + height: 64px; + display: flex; + align-items: center; +} + +#hyper-checkout-payment-merchant-details { + margin-top: 5px; +} + +.hyper-checkout-payment-merchant-name { + font-weight: 600; + font-size: 19px; +} + +.hyper-checkout-payment-ref { + font-size: 12px; + margin-top: 5px; +} + +.hyper-checkout-image-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +#hyper-checkout-merchant-image, +#hyper-checkout-cart-image { + height: 64px; + width: 64px; + border-radius: 4px; + display: flex; + align-self: flex-start; + align-items: center; + justify-content: center; +} + +#hyper-checkout-merchant-image > img { + height: 48px; + width: 48px; +} + +#hyper-checkout-cart-image { + display: none; + cursor: pointer; + height: 60px; + width: 60px; + border-radius: 100px; + background-color: #f5f5f5; +} + +#hyper-checkout-payment-footer { + margin-top: 20px; + background-color: #f5f5f5; + font-size: 13px; + font-weight: 500; + padding: 12px 20px; + border-radius: 0 0 8px 8px; +} + +#hyper-checkout-cart { + display: flex; + flex-flow: column; + min-width: 600px; + margin-top: 40px; + max-height: 60vh; +} + +#hyper-checkout-cart-items { + max-height: 291px; + overflow: scroll; + transition: all 0.3s ease; +} + +.hyper-checkout-cart-header { + font-size: 15px; + display: flex; + flex-flow: row; + align-items: center; +} + +.hyper-checkout-cart-header > span { + margin-left: 5px; + font-weight: 500; +} + +.cart-close { + display: none; + cursor: pointer; +} + +.hyper-checkout-cart-item { + display: flex; + flex-flow: row; + padding: 20px 0; + font-size: 15px; +} + +.hyper-checkout-cart-product-image { + height: 56px; + width: 56px; + border-radius: 4px; +} + +.hyper-checkout-card-item-name { + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + display: -webkit-box; + -webkit-box-orient: vertical; +} + +.hyper-checkout-card-item-quantity { + border: 1px solid #e6e6e6; + border-radius: 3px; + width: max-content; + padding: 5px 12px; + background-color: #fafafa; + font-size: 13px; + font-weight: 500; +} + +.hyper-checkout-cart-product-details { + display: flex; + flex-flow: column; + margin-left: 15px; + justify-content: space-between; + width: 100%; +} + +.hyper-checkout-card-item-price { + justify-self: flex-end; + font-weight: 600; + font-size: 16px; + padding-left: 30px; + text-align: end; + min-width: max-content; +} + +.hyper-checkout-cart-item-divider { + height: 1px; + background-color: #e6e6e6; +} + +.hyper-checkout-cart-button { + font-size: 12px; + font-weight: 500; + cursor: pointer; + align-self: flex-start; + display: flex; + align-content: flex-end; + gap: 3px; + text-decoration: none; + transition: text-decoration 0.3s; + margin-top: 10px; +} + +.hyper-checkout-cart-button:hover { + text-decoration: underline; +} + +#hyper-checkout-merchant-description { + font-size: 13px; + color: #808080; +} + +.powered-by-hyper { + margin-top: 40px; + align-self: flex-start; +} + +.hyper-checkout-sdk { + width: 50vw; + min-width: 584px; + z-index: 2; + background-color: var(--primary-color); + box-shadow: 0px 1px 10px #f2f2f2; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; +} + +#payment-form-wrap { + min-width: 300px; + width: 30vw; + padding: 20px; + background-color: white; + border-radius: 3px; +} + +#hyper-checkout-sdk-header { + padding: 10px 10px 10px 22px; + display: flex; + align-items: flex-start; + justify-content: flex-start; + border-bottom: 1px solid #f2f2f2; +} + +.hyper-checkout-sdk-header-logo { + height: 60px; + width: 60px; + background-color: white; + border-radius: 2px; +} + +.hyper-checkout-sdk-header-logo > img { + height: 56px; + width: 56px; + margin: 2px; +} + +.hyper-checkout-sdk-header-items { + display: flex; + flex-flow: column; + color: white; + font-size: 20px; + font-weight: 700; +} + +.hyper-checkout-sdk-items { + margin-left: 10px; +} + +.hyper-checkout-sdk-header-brand-name, +.hyper-checkout-sdk-header-amount { + font-size: 18px; + font-weight: 600; + display: flex; + align-items: center; + font-family: "Montserrat"; + justify-self: flex-start; +} + +.hyper-checkout-sdk-header-amount { + font-weight: 800; + font-size: 25px; +} + +.page-spinner { + position: absolute; + width: 100vw; + height: 100vh; + z-index: 3; + background-color: #fff; + display: flex; + align-items: center; + justify-content: center; +} + +.sdk-spinner { + width: 100%; + height: 100%; + z-index: 3; + background-color: #fff; + display: flex; + align-items: center; + justify-content: center; +} + +.spinner { + width: 60px; + height: 60px; +} + +.spinner div { + transform-origin: 30px 30px; + animation: spinner 1.2s linear infinite; +} + +.spinner div:after { + content: " "; + display: block; + position: absolute; + top: 3px; + left: 28px; + width: 4px; + height: 15px; + border-radius: 20%; + background: var(--primary-color); +} + +.spinner div:nth-child(1) { + transform: rotate(0deg); + animation-delay: -1.1s; +} + +.spinner div:nth-child(2) { + transform: rotate(30deg); + animation-delay: -1s; +} + +.spinner div:nth-child(3) { + transform: rotate(60deg); + animation-delay: -0.9s; +} + +.spinner div:nth-child(4) { + transform: rotate(90deg); + animation-delay: -0.8s; +} + +.spinner div:nth-child(5) { + transform: rotate(120deg); + animation-delay: -0.7s; +} + +.spinner div:nth-child(6) { + transform: rotate(150deg); + animation-delay: -0.6s; +} + +.spinner div:nth-child(7) { + transform: rotate(180deg); + animation-delay: -0.5s; +} + +.spinner div:nth-child(8) { + transform: rotate(210deg); + animation-delay: -0.4s; +} + +.spinner div:nth-child(9) { + transform: rotate(240deg); + animation-delay: -0.3s; +} + +.spinner div:nth-child(10) { + transform: rotate(270deg); + animation-delay: -0.2s; +} + +.spinner div:nth-child(11) { + transform: rotate(300deg); + animation-delay: -0.1s; +} + +.spinner div:nth-child(12) { + transform: rotate(330deg); + animation-delay: 0s; +} + +@keyframes spinner { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +#hyper-checkout-status-canvas { + width: 100%; + height: 100%; + justify-content: center; + align-items: center; + background-color: var(--primary-color); +} + +.hyper-checkout-status-wrap { + display: flex; + flex-flow: column; + font-family: "Montserrat"; + width: auto; + min-width: 400px; + background-color: white; + border-radius: 5px; +} + +#hyper-checkout-status-header { + max-width: 1200px; + border-radius: 3px; + border-bottom: 1px solid #e6e6e6; +} + +#hyper-checkout-status-header, +#hyper-checkout-status-content { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 24px; + font-weight: 600; + padding: 15px 20px; +} + +.hyper-checkout-status-amount { + font-family: "Montserrat"; + font-size: 35px; + font-weight: 700; +} + +.hyper-checkout-status-merchant-logo { + border: 1px solid #e6e6e6; + border-radius: 5px; + padding: 9px; + height: 48px; + width: 48px; +} + +#hyper-checkout-status-content { + height: 100%; + flex-flow: column; + min-height: 500px; + align-items: center; + justify-content: center; +} + +.hyper-checkout-status-image { + height: 200px; + width: 200px; +} + +.hyper-checkout-status-text { + text-align: center; + font-size: 21px; + font-weight: 600; + margin-top: 20px; +} + +.hyper-checkout-status-message { + text-align: center; + font-size: 12px !important; + margin-top: 10px; + font-size: 14px; + font-weight: 500; + max-width: 400px; +} + +.hyper-checkout-status-details { + display: flex; + flex-flow: column; + margin-top: 20px; + border-radius: 3px; + border: 1px solid #e6e6e6; + max-width: calc(100vw - 40px); +} + +.hyper-checkout-status-item { + display: flex; + align-items: center; + padding: 5px 10px; + border-bottom: 1px solid #e6e6e6; + word-wrap: break-word; +} + +.hyper-checkout-status-item:last-child { + border-bottom: 0; +} + +.hyper-checkout-item-header { + min-width: 13ch; + font-size: 12px; +} + +.hyper-checkout-item-value { + font-size: 12px; + overflow-x: hidden; + overflow-y: auto; + word-wrap: break-word; + font-weight: 400; +} + +#hyper-checkout-status-redirect-message { + margin-top: 20px; + font-family: "Montserrat"; + font-size: 13px; +} + +@keyframes loading { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes slide-from-right { + from { + right: -582px; + } + + to { + right: 0; + } +} + +@keyframes slide-to-right { + from { + right: 0; + } + + to { + right: -582px; + } +} + +#payment-message { + font-size: 12px; + font-weight: 500; + padding: 2%; + color: #ff0000; + font-family: "Montserrat"; +} + +#payment-form { + max-width: 560px; + width: 100%; + min-height: 500px; + max-height: 90vh; + height: 100%; + overflow: scroll; + margin: 0 auto; + text-align: center; +} + +#submit { + cursor: pointer; + margin-top: 20px; + width: 100%; + height: 38px; + background-color: var(--primary-color); + border: 0; + border-radius: 4px; + font-size: 18px; + display: flex; + justify-content: center; + align-items: center; +} + +#submit.disabled { + cursor: not-allowed; +} + +#submit-spinner { + width: 28px; + height: 28px; + border: 4px solid #fff; + border-bottom-color: #ff3d00; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: loading 1s linear infinite; +} + +@media only screen and (max-width: 1400px) { + body { + overflow-y: scroll; + } + + .hyper-checkout { + flex-flow: column; + margin: 0; + height: auto; + overflow: visible; + } + + #hyper-checkout-payment-merchant-details { + margin-top: 20px; + } + + .main { + width: auto; + min-width: 300px; + } + + .hyper-checkout-payment { + min-width: 300px; + width: calc(100vw - 50px); + margin: 0; + padding: 25px; + border: 0; + border-radius: 0; + background-color: var(--primary-color); + display: flex; + flex-flow: column; + justify-self: flex-start; + align-self: flex-start; + } + + .hyper-checkout-payment-content-details { + max-width: 520px; + width: 100%; + align-self: center; + margin-bottom: 0; + } + + .content-details-wrap { + flex-flow: column; + flex-direction: column-reverse; + margin: 0; + } + + #hyper-checkout-merchant-image { + background-color: white; + } + + #hyper-checkout-cart-image { + display: flex; + } + + .hyper-checkout-payment-price { + font-size: 48px; + margin-top: 20px; + } + + .hyper-checkout-payment-merchant-name { + font-size: 18px; + } + + #hyper-checkout-payment-footer { + border-radius: 50px; + width: max-content; + padding: 10px 20px; + } + + #hyper-checkout-cart { + position: absolute; + top: 0; + right: 0; + z-index: 100; + margin: 0; + min-width: 300px; + max-width: 582px; + max-height: 100vh; + width: 100vw; + height: 100vh; + background-color: #f5f5f5; + box-shadow: 0px 10px 10px #aeaeae; + right: 0px; + animation: slide-from-right 0.3s linear; + } + + .hyper-checkout-cart-header { + margin: 10px 0 0 10px; + } + + .cart-close { + margin: 0 10px 0 auto; + display: inline; + } + + #hyper-checkout-cart-items { + margin: 20px 20px 0 20px; + padding: 0; + } + + .hyper-checkout-cart-button { + margin: 10px; + text-align: right; + } + + .powered-by-hyper { + display: none; + } + + #hyper-checkout-sdk { + background-color: transparent; + width: auto; + min-width: 300px; + box-shadow: none; + } + + #payment-form-wrap { + min-width: 300px; + width: calc(100vw - 40px); + margin: 0; + padding: 25px 20px; + } + + #hyper-checkout-status-canvas { + background-color: #fefefe; + } + + .hyper-checkout-status-wrap { + min-width: 100vw; + width: 100vw; + } + + #hyper-checkout-status-header { + max-width: calc(100% - 40px); + } +} diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html new file mode 100644 index 000000000000..be4369be1d97 --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html @@ -0,0 +1,197 @@ + + + + + + Payments requested by HyperSwitch + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+ + + + Your Cart + + + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ + + {{ hyperloader_sdk_link }} + + diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js new file mode 100644 index 000000000000..75e063c3a991 --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js @@ -0,0 +1,906 @@ +// @ts-check + +/** + * UTIL FUNCTIONS + */ + +function adjustLightness(hexColor, factor) { + // Convert hex to RGB + var r = parseInt(hexColor.slice(1, 3), 16); + var g = parseInt(hexColor.slice(3, 5), 16); + var b = parseInt(hexColor.slice(5, 7), 16); + + // Convert RGB to HSL + var hsl = rgbToHsl(r, g, b); + + // Adjust lightness + hsl[2] = Math.max(0, Math.min(100, hsl[2] * factor)); + + // Convert HSL back to RGB + var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]); + + // Convert RGB to hex + var newHexColor = rgbToHex(rgb[0], rgb[1], rgb[2]); + + return newHexColor; +} +function rgbToHsl(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + var max = Math.max(r, g, b), + min = Math.min(r, g, b); + var h = 1, + s, + l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return [h * 360, s * 100, l * 100]; +} +function hslToRgb(h, s, l) { + h /= 360; + s /= 100; + l /= 100; + var r, g, b; + + if (s === 0) { + r = g = b = l; + } else { + var hue2rgb = function (p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [r * 255, g * 255, b * 255]; +} +function rgbToHex(r, g, b) { + var toHex = function (c) { + var hex = Math.round(c).toString(16); + return hex.length === 1 ? "0" + hex : hex; + }; + return "#" + toHex(r) + toHex(g) + toHex(b); +} + +/** + * Ref - https://github.com/onury/invert-color/blob/master/lib/cjs/invert.js + */ +function padz(str, len) { + if (len === void 0) { + len = 2; + } + return (new Array(len).join("0") + str).slice(-len); +} +function hexToRgbArray(hex) { + if (hex.slice(0, 1) === "#") hex = hex.slice(1); + var RE_HEX = /^(?:[0-9a-f]{3}){1,2}$/i; + if (!RE_HEX.test(hex)) throw new Error('Invalid HEX color: "' + hex + '"'); + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + return [ + parseInt(hex.slice(0, 2), 16), + parseInt(hex.slice(2, 4), 16), + parseInt(hex.slice(4, 6), 16), + ]; +} +function toRgbArray(c) { + if (!c) throw new Error("Invalid color value"); + if (Array.isArray(c)) return c; + return typeof c === "string" ? hexToRgbArray(c) : [c.r, c.g, c.b]; +} +function getLuminance(c) { + var i, x; + var a = []; + for (i = 0; i < c.length; i++) { + x = c[i] / 255; + a[i] = x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); + } + return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2]; +} +function invertToBW(color, bw, asArr) { + var DEFAULT_BW = { + black: "#090302", + white: "#FFFFFC", + threshold: Math.sqrt(1.05 * 0.05) - 0.05, + }; + var options = bw === true ? DEFAULT_BW : Object.assign({}, DEFAULT_BW, bw); + return getLuminance(color) > options.threshold + ? asArr + ? hexToRgbArray(options.black) + : options.black + : asArr + ? hexToRgbArray(options.white) + : options.white; +} +function invert(color, bw) { + if (bw === void 0) { + bw = false; + } + color = toRgbArray(color); + if (bw) return invertToBW(color, bw); + return ( + "#" + + color + .map(function (c) { + return padz((255 - c).toString(16)); + }) + .join("") + ); +} + +/** + * UTIL FUNCTIONS END HERE + */ + +{{ payment_details_js_script }} + +// @ts-ignore +window.state = { + prevHeight: window.innerHeight, + prevWidth: window.innerWidth, + isMobileView: window.innerWidth <= 1400, + currentScreen: "payment_link", +}; + +var widgets = null; +var unifiedCheckout = null; +// @ts-ignore +var pub_key = window.__PAYMENT_DETAILS.pub_key; +var hyper = null; + +/** + * Trigger - init function invoked once the script tag is loaded + * Use + * - Update document's title + * - Update document's icon + * - Render and populate document with payment details and cart + * - Initialize event listeners for updating UI on screen size changes + * - Initialize SDK + **/ +function boot() { + // @ts-ignore + var paymentDetails = window.__PAYMENT_DETAILS; + + if (paymentDetails.merchant_name) { + document.title = "Payment requested by " + paymentDetails.merchant_name; + } + + if (paymentDetails.merchant_logo) { + var link = document.createElement("link"); + link.rel = "icon"; + link.href = paymentDetails.merchant_logo; + link.type = "image/x-icon"; + document.head.appendChild(link); + } + + // Render UI + renderPaymentDetails(paymentDetails); + renderSDKHeader(paymentDetails); + renderCart(paymentDetails); + + // Deal w loaders + show("#sdk-spinner"); + hide("#page-spinner"); + hide("#unified-checkout"); + + // Add event listeners + initializeEventListeners(paymentDetails); + + // Initialize SDK + // @ts-ignore + if (window.Hyper) { + initializeSDK(); + } + + // State specific functions + // @ts-ignore + if (window.state.isMobileView) { + show("#hyper-footer"); + hide("#hyper-checkout-cart"); + } else { + show("#hyper-checkout-cart"); + } +} +boot(); + +/** + * Use - add event listeners for changing UI on screen resize + * @param {PaymentDetails} paymentDetails + */ +function initializeEventListeners(paymentDetails) { + var primaryColor = paymentDetails.theme; + var lighterColor = adjustLightness(primaryColor, 1.4); + var darkerColor = adjustLightness(primaryColor, 0.8); + var contrastBWColor = invert(primaryColor, true); + var a = lighterColor.match(/[fF]/gi); + var contrastingTone = + Array.isArray(a) && a.length > 4 ? darkerColor : lighterColor; + var hyperCheckoutNode = document.getElementById("hyper-checkout-payment"); + var hyperCheckoutCartImageNode = document.getElementById( + "hyper-checkout-cart-image" + ); + var hyperCheckoutFooterNode = document.getElementById( + "hyper-checkout-payment-footer" + ); + var submitButtonNode = document.getElementById("submit"); + var submitButtonLoaderNode = document.getElementById("submit-spinner"); + + if (submitButtonLoaderNode instanceof HTMLSpanElement) { + submitButtonLoaderNode.style.borderBottomColor = contrastingTone; + } + + if (submitButtonNode instanceof HTMLButtonElement) { + submitButtonNode.style.color = contrastBWColor; + } + + if (hyperCheckoutCartImageNode instanceof HTMLDivElement) { + hyperCheckoutCartImageNode.style.backgroundColor = contrastingTone; + } + + if (window.innerWidth <= 1400) { + if (hyperCheckoutNode instanceof HTMLDivElement) { + hyperCheckoutNode.style.color = contrastBWColor; + } + if (hyperCheckoutFooterNode instanceof HTMLDivElement) { + hyperCheckoutFooterNode.style.backgroundColor = contrastingTone; + } + } else if (window.innerWidth > 1400) { + if (hyperCheckoutNode instanceof HTMLDivElement) { + hyperCheckoutNode.style.color = "#333333"; + } + if (hyperCheckoutFooterNode instanceof HTMLDivElement) { + hyperCheckoutFooterNode.style.backgroundColor = "#F5F5F5"; + } + } + + window.addEventListener("resize", function (event) { + var currentHeight = window.innerHeight; + var currentWidth = window.innerWidth; + // @ts-ignore + if (currentWidth <= 1400 && window.state.prevWidth > 1400) { + hide("#hyper-checkout-cart"); + // @ts-ignore + if (window.state.currentScreen === "payment_link") { + show("#hyper-footer"); + } + try { + if (hyperCheckoutNode instanceof HTMLDivElement) { + hyperCheckoutNode.style.color = contrastBWColor; + } + if (hyperCheckoutFooterNode instanceof HTMLDivElement) { + hyperCheckoutFooterNode.style.backgroundColor = lighterColor; + } + } catch (error) { + console.error("Failed to fetch primary-color, using default", error); + } + // @ts-ignore + } else if (currentWidth > 1400 && window.state.prevWidth <= 1400) { + // @ts-ignore + if (window.state.currentScreen === "payment_link") { + hide("#hyper-footer"); + } + show("#hyper-checkout-cart"); + try { + if (hyperCheckoutNode instanceof HTMLDivElement) { + hyperCheckoutNode.style.color = "#333333"; + } + if (hyperCheckoutFooterNode instanceof HTMLDivElement) { + hyperCheckoutFooterNode.style.backgroundColor = "#F5F5F5"; + } + } catch (error) { + console.error("Failed to revert back to default colors", error); + } + } + + // @ts-ignore + window.state.prevHeight = currentHeight; + // @ts-ignore + window.state.prevWidth = currentWidth; + // @ts-ignore + window.state.isMobileView = currentWidth <= 1400; + }); +} + +/** + * Trigger - post mounting SDK + * Use - set relevant classes to elements in the doc for showing SDK + **/ +function showSDK() { + show("#hyper-checkout-sdk"); + show("#hyper-checkout-details"); + show("#submit"); + show("#unified-checkout"); + hide("#sdk-spinner"); +} + +/** + * Trigger - post downloading SDK + * Uses + * - Instantiate SDK + * - Create a payment widget + * - Decide whether or not to show SDK (based on status) + **/ +function initializeSDK() { + // @ts-ignore + var paymentDetails = window.__PAYMENT_DETAILS; + var client_secret = paymentDetails.client_secret; + var appearance = { + variables: { + colorPrimary: paymentDetails.theme || "rgb(0, 109, 249)", + fontFamily: "Work Sans, sans-serif", + fontSizeBase: "16px", + colorText: "rgb(51, 65, 85)", + colorTextSecondary: "#334155B3", + colorPrimaryText: "rgb(51, 65, 85)", + colorTextPlaceholder: "#33415550", + borderColor: "#33415550", + colorBackground: "rgb(255, 255, 255)", + }, + }; + // @ts-ignore + hyper = window.Hyper(pub_key, { + isPreloadEnabled: false, + }); + widgets = hyper.widgets({ + appearance: appearance, + clientSecret: client_secret, + }); + var type = + paymentDetails.sdk_layout === "spaced_accordion" || + paymentDetails.sdk_layout === "accordion" + ? "accordion" + : paymentDetails.sdk_layout; + + var unifiedCheckoutOptions = { + disableSaveCards: true, + layout: { + type: type, //accordion , tabs, spaced accordion + spacedAccordionItems: paymentDetails.sdk_layout === "spaced_accordion", + }, + branding: "never", + wallets: { + walletReturnUrl: paymentDetails.return_url, + style: { + theme: "dark", + type: "default", + height: 55, + }, + }, + }; + unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions); + mountUnifiedCheckout("#unified-checkout"); + showSDK(); +} + +/** + * Use - mount payment widget on the passed element + * @param {String} id + **/ +function mountUnifiedCheckout(id) { + if (unifiedCheckout !== null) { + unifiedCheckout.mount(id); + } +} + +/** + * Trigger - on clicking submit button + * Uses + * - Trigger /payment/confirm through SDK + * - Toggle UI loaders appropriately + * - Handle errors and redirect to status page + * @param {Event} e + */ +function handleSubmit(e) { + // @ts-ignore + var paymentDetails = window.__PAYMENT_DETAILS; + + // Update button loader + hide("#submit-button-text"); + show("#submit-spinner"); + var submitButtonNode = document.getElementById("submit"); + if (submitButtonNode instanceof HTMLButtonElement) { + submitButtonNode.disabled = true; + submitButtonNode.classList.add("disabled"); + } + + hyper + .confirmPayment({ + widgets: widgets, + confirmParams: { + // Make sure to change this to your payment completion page + return_url: paymentDetails.return_url, + }, + }) + .then(function (result) { + var error = result.error; + if (error) { + if (error.type === "validation_error") { + showMessage(error.message); + } else { + showMessage("An unexpected error occurred."); + } + } else { + redirectToStatus(); + } + }) + .catch(function (error) { + console.error("Error confirming payment_intent", error); + }) + .finally(() => { + hide("#submit-spinner"); + show("#submit-button-text"); + if (submitButtonNode instanceof HTMLButtonElement) { + submitButtonNode.disabled = false; + submitButtonNode.classList.remove("disabled"); + } + }); +} + +function show(id) { + removeClass(id, "hidden"); +} +function hide(id) { + addClass(id, "hidden"); +} + +function showMessage(msg) { + show("#payment-message"); + addText("#payment-message", msg); +} + +/** + * Use - redirect to /payment_link/status + */ +function redirectToStatus() { + var arr = window.location.pathname.split("/"); + arr.splice(0, 2); + arr.unshift("status"); + arr.unshift("payment_link"); + window.location.href = window.location.origin + "/" + arr.join("/"); +} + +function addText(id, msg) { + var element = document.querySelector(id); + element.innerText = msg; +} + +function addClass(id, className) { + var element = document.querySelector(id); + element.classList.add(className); +} + +function removeClass(id, className) { + var element = document.querySelector(id); + element.classList.remove(className); +} + +/** + * Use - format date in "hh:mm AM/PM timezone MM DD, YYYY" + * @param {Date} date + **/ +function formatDate(date) { + var months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + + var hours = date.getHours(); + var minutes = date.getMinutes(); + minutes = minutes < 10 ? "0" + minutes : minutes; + var suffix = hours > 11 ? "PM" : "AM"; + hours = hours % 12; + hours = hours ? hours : 12; + var day = date.getDate(); + var month = months[date.getMonth()]; + var year = date.getUTCFullYear(); + + // @ts-ignore + var locale = navigator.language || navigator.userLanguage; + var timezoneShorthand = date + .toLocaleDateString(locale, { + day: "2-digit", + timeZoneName: "long", + }) + .substring(4) + .split(" ") + .reduce(function (tz, c) { + return tz + c.charAt(0).toUpperCase(); + }, ""); + + var formatted = + hours + + ":" + + minutes + + " " + + suffix + + " " + + timezoneShorthand + + " " + + month + + " " + + day + + ", " + + year; + return formatted; +} + +/** + * Trigger - on boot + * Uses + * - Render payment related details (header bit) + * - Amount + * - Merchant's name + * - Expiry + * @param {PaymentDetails} paymentDetails + **/ +function renderPaymentDetails(paymentDetails) { + // Create price node + var priceNode = document.createElement("div"); + priceNode.className = "hyper-checkout-payment-price"; + priceNode.innerText = paymentDetails.currency + " " + paymentDetails.amount; + + // Create merchant name's node + var merchantNameNode = document.createElement("div"); + merchantNameNode.className = "hyper-checkout-payment-merchant-name"; + merchantNameNode.innerText = "Requested by " + paymentDetails.merchant_name; + + // Create payment ID node + var paymentIdNode = document.createElement("div"); + paymentIdNode.className = "hyper-checkout-payment-ref"; + paymentIdNode.innerText = "Ref Id: " + paymentDetails.payment_id; + + // Create merchant logo's node + var merchantLogoNode = document.createElement("img"); + merchantLogoNode.src = paymentDetails.merchant_logo; + + // Create expiry node + var paymentExpiryNode = document.createElement("div"); + paymentExpiryNode.className = "hyper-checkout-payment-footer-expiry"; + var expiryDate = new Date(paymentDetails.session_expiry); + var formattedDate = formatDate(expiryDate); + paymentExpiryNode.innerText = "Link expires on: " + formattedDate; + + // Append information to DOM + var paymentContextNode = document.getElementById( + "hyper-checkout-payment-context" + ); + if (paymentContextNode instanceof HTMLDivElement) { + paymentContextNode.prepend(priceNode); + } + var paymentMerchantDetails = document.getElementById( + "hyper-checkout-payment-merchant-details" + ); + if (paymentMerchantDetails instanceof HTMLDivElement) { + paymentMerchantDetails.append(merchantNameNode); + paymentMerchantDetails.append(paymentIdNode); + } + var merchantImageNode = document.getElementById( + "hyper-checkout-merchant-image" + ); + if (merchantImageNode instanceof HTMLDivElement) { + merchantImageNode.prepend(merchantLogoNode); + } + var footerNode = document.getElementById("hyper-checkout-payment-footer"); + if (footerNode instanceof HTMLDivElement) { + footerNode.append(paymentExpiryNode); + } +} + +/** + * Trigger - on boot + * Uses + * - Render cart wrapper and items + * - Attaches an onclick event for toggling expand on the items list + * @param {PaymentDetails} paymentDetails + **/ +function renderCart(paymentDetails) { + var orderDetails = paymentDetails.order_details; + + // Cart items + if (Array.isArray(orderDetails) && orderDetails.length > 0) { + var cartNode = document.getElementById("hyper-checkout-cart"); + var cartItemsNode = document.getElementById("hyper-checkout-cart-items"); + var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = + paymentDetails.max_items_visible_after_collapse; + + orderDetails.map(function (item, index) { + if (index >= MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { + return; + } + renderCartItem( + item, + paymentDetails, + index !== 0 && index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE, + cartItemsNode + ); + }); + // Expand / collapse button + var totalItems = orderDetails.length; + if (totalItems > MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { + var expandButtonNode = document.createElement("div"); + expandButtonNode.className = "hyper-checkout-cart-button"; + expandButtonNode.onclick = () => { + handleCartView(paymentDetails); + }; + var buttonImageNode = document.createElement("svg"); + buttonImageNode.id = "hyper-checkout-cart-button-arrow"; + var arrowDownImage = document.getElementById("arrow-down"); + if (arrowDownImage instanceof Object) { + buttonImageNode.innerHTML = arrowDownImage.innerHTML; + } + var buttonTextNode = document.createElement("span"); + buttonTextNode.id = "hyper-checkout-cart-button-text"; + var hiddenItemsCount = + orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE; + buttonTextNode.innerText = "Show More (" + hiddenItemsCount + ")"; + expandButtonNode.append(buttonTextNode, buttonImageNode); + if (cartNode instanceof HTMLDivElement) { + cartNode.insertBefore(expandButtonNode, cartNode.lastElementChild); + } + } + } else { + hide("#hyper-checkout-cart-header"); + hide("#hyper-checkout-cart-items"); + hide("#hyper-checkout-cart-image"); + if ( + typeof paymentDetails.merchant_description === "string" && + paymentDetails.merchant_description.length > 0 + ) { + var merchantDescriptionNode = document.getElementById( + "hyper-checkout-merchant-description" + ); + if (merchantDescriptionNode instanceof HTMLDivElement) { + merchantDescriptionNode.innerText = paymentDetails.merchant_description; + } + show("#hyper-checkout-merchant-description"); + } + } +} + +/** + * Trigger - on cart render + * Uses + * - Renders a single cart item which includes + * - Product image + * - Product name + * - Quantity + * - Single item amount + * @param {OrderDetailsWithAmount} item + * @param {PaymentDetails} paymentDetails + * @param {boolean} shouldAddDividerNode + * @param {HTMLDivElement} cartItemsNode + **/ +function renderCartItem( + item, + paymentDetails, + shouldAddDividerNode, + cartItemsNode +) { + // Wrappers + var itemWrapperNode = document.createElement("div"); + itemWrapperNode.className = "hyper-checkout-cart-item"; + var nameAndQuantityWrapperNode = document.createElement("div"); + nameAndQuantityWrapperNode.className = "hyper-checkout-cart-product-details"; + // Image + var productImageNode = document.createElement("img"); + productImageNode.className = "hyper-checkout-cart-product-image"; + productImageNode.src = item.product_img_link; + // Product title + var productNameNode = document.createElement("div"); + productNameNode.className = "hyper-checkout-card-item-name"; + productNameNode.innerText = item.product_name; + // Product quantity + var quantityNode = document.createElement("div"); + quantityNode.className = "hyper-checkout-card-item-quantity"; + quantityNode.innerText = "Qty: " + item.quantity; + // Product price + var priceNode = document.createElement("div"); + priceNode.className = "hyper-checkout-card-item-price"; + priceNode.innerText = paymentDetails.currency + " " + item.amount; + // Append items + nameAndQuantityWrapperNode.append(productNameNode, quantityNode); + itemWrapperNode.append( + productImageNode, + nameAndQuantityWrapperNode, + priceNode + ); + if (shouldAddDividerNode) { + var dividerNode = document.createElement("div"); + dividerNode.className = "hyper-checkout-cart-item-divider"; + cartItemsNode.append(dividerNode); + } + cartItemsNode.append(itemWrapperNode); +} + +/** + * Trigger - on toggling expansion of cart list + * Uses + * - Render or delete items based on current state of the rendered cart list + * @param {PaymentDetails} paymentDetails + **/ +function handleCartView(paymentDetails) { + 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" + ); + var dividerHTMLCollection = document.getElementsByClassName( + "hyper-checkout-cart-item-divider" + ); + var cartItems = [].slice.call(itemsHTMLCollection); + var dividerItems = [].slice.call(dividerHTMLCollection); + var isHidden = cartItems.length < orderDetails.length; + var cartItemsNode = document.getElementById("hyper-checkout-cart-items"); + var cartButtonTextNode = document.getElementById( + "hyper-checkout-cart-button-text" + ); + var cartButtonImageNode = document.getElementById( + "hyper-checkout-cart-button-arrow" + ); + if (isHidden) { + if (Array.isArray(orderDetails)) { + orderDetails.map(function (item, index) { + if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { + return; + } + renderCartItem( + item, + paymentDetails, + index >= MAX_ITEMS_VISIBLE_AFTER_COLLAPSE, + cartItemsNode + ); + }); + } + if (cartItemsNode instanceof HTMLDivElement) { + cartItemsNode.style.maxHeight = cartItemsNode.scrollHeight + "px"; + cartItemsNode.style.height = cartItemsNode.scrollHeight + "px"; + } + if (cartButtonTextNode instanceof HTMLButtonElement) { + cartButtonTextNode.innerText = "Show Less"; + } + var arrowUpImage = document.getElementById("arrow-up"); + if ( + cartButtonImageNode instanceof Object && + arrowUpImage instanceof Object + ) { + cartButtonImageNode.innerHTML = arrowUpImage.innerHTML; + } + } else { + if (cartItemsNode instanceof HTMLDivElement) { + cartItemsNode.style.maxHeight = "300px"; + cartItemsNode.style.height = "290px"; + cartItemsNode.scrollTo({ top: 0, behavior: "smooth" }); + setTimeout(function () { + cartItems.map(function (item, index) { + if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { + return; + } + if (cartItemsNode instanceof HTMLDivElement) { + cartItemsNode.removeChild(item); + } + }); + dividerItems.map(function (item, index) { + if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE - 1) { + return; + } + if (cartItemsNode instanceof HTMLDivElement) { + cartItemsNode.removeChild(item); + } + }); + }, 300); + } + setTimeout(function () { + var hiddenItemsCount = + orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE; + if (cartButtonTextNode instanceof HTMLButtonElement) { + cartButtonTextNode.innerText = "Show More (" + hiddenItemsCount + ")"; + } + var arrowDownImage = document.getElementById("arrow-down"); + if ( + cartButtonImageNode instanceof Object && + arrowDownImage instanceof Object + ) { + cartButtonImageNode.innerHTML = arrowDownImage.innerHTML; + } + }, 250); + } +} + +/** + * Use - hide cart when in mobile view + **/ +function hideCartInMobileView() { + window.history.back(); + var cartNode = document.getElementById("hyper-checkout-cart"); + if (cartNode instanceof HTMLDivElement) { + cartNode.style.animation = "slide-to-right 0.3s linear"; + cartNode.style.right = "-582px"; + } + setTimeout(function () { + hide("#hyper-checkout-cart"); + }, 300); +} + +/** + * Use - show cart when in mobile view + **/ +function viewCartInMobileView() { + window.history.pushState("view-cart", ""); + var cartNode = document.getElementById("hyper-checkout-cart"); + if (cartNode instanceof HTMLDivElement) { + cartNode.style.animation = "slide-from-right 0.3s linear"; + cartNode.style.right = "0px"; + } + show("#hyper-checkout-cart"); +} + +/** + * Trigger - on boot + * Uses + * - Render SDK header node + * - merchant's name + * - currency + amount + * @param {PaymentDetails} paymentDetails + **/ +function renderSDKHeader(paymentDetails) { + // SDK headers' items + var sdkHeaderItemNode = document.createElement("div"); + sdkHeaderItemNode.className = "hyper-checkout-sdk-items"; + var sdkHeaderMerchantNameNode = document.createElement("div"); + sdkHeaderMerchantNameNode.className = "hyper-checkout-sdk-header-brand-name"; + sdkHeaderMerchantNameNode.innerText = paymentDetails.merchant_name; + var sdkHeaderAmountNode = document.createElement("div"); + sdkHeaderAmountNode.className = "hyper-checkout-sdk-header-amount"; + sdkHeaderAmountNode.innerText = + paymentDetails.currency + " " + paymentDetails.amount; + sdkHeaderItemNode.append(sdkHeaderMerchantNameNode); + sdkHeaderItemNode.append(sdkHeaderAmountNode); + + // Append to SDK header's node + var sdkHeaderNode = document.getElementById("hyper-checkout-sdk-header"); + if (sdkHeaderNode instanceof HTMLDivElement) { + // sdkHeaderNode.append(sdkHeaderLogoNode); + sdkHeaderNode.append(sdkHeaderItemNode); + } +} diff --git a/crates/router/src/core/payment_link/payment_link_status/status.css b/crates/router/src/core/payment_link/payment_link_status/status.css new file mode 100644 index 000000000000..113f13531381 --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_status/status.css @@ -0,0 +1,161 @@ +{{ css_color_scheme }} + +body, +body > div { + height: 100vh; + width: 100vw; +} + +body { + font-family: "Montserrat"; + background-color: var(--primary-color); + color: #333; + text-align: center; + margin: 0; + padding: 0; + overflow: hidden; +} + +body > div { + height: 100vh; + width: 100vw; + overflow: scroll; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; +} + +.hyper-checkout-status-wrap { + display: flex; + flex-flow: column; + font-family: "Montserrat"; + width: auto; + min-width: 400px; + max-width: 800px; + background-color: white; + border-radius: 5px; +} + +#hyper-checkout-status-header { + max-width: 1200px; + border-radius: 3px; + border-bottom: 1px solid #e6e6e6; +} + +#hyper-checkout-status-header, +#hyper-checkout-status-content { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 24px; + font-weight: 600; + padding: 15px 20px; +} + +.hyper-checkout-status-amount { + font-family: "Montserrat"; + font-size: 35px; + font-weight: 700; +} + +.hyper-checkout-status-merchant-logo { + border: 1px solid #e6e6e6; + border-radius: 5px; + padding: 9px; + height: 48px; + width: 48px; +} + +#hyper-checkout-status-content { + height: 100%; + flex-flow: column; + min-height: 500px; + align-items: center; + justify-content: center; +} + +.hyper-checkout-status-image { + height: 200px; + width: 200px; +} + +.hyper-checkout-status-text { + text-align: center; + font-size: 21px; + font-weight: 600; + margin-top: 20px; +} + +.hyper-checkout-status-message { + text-align: center; + font-size: 12px !important; + margin-top: 10px; + font-size: 14px; + font-weight: 500; + max-width: 400px; +} + +.hyper-checkout-status-details { + display: flex; + flex-flow: column; + margin-top: 20px; + border-radius: 3px; + border: 1px solid #e6e6e6; + max-width: calc(100vw - 40px); +} + +.hyper-checkout-status-item { + display: flex; + align-items: center; + padding: 5px 10px; + border-bottom: 1px solid #e6e6e6; + word-wrap: break-word; +} + +.hyper-checkout-status-item:last-child { + border-bottom: 0; +} + +.hyper-checkout-item-header { + min-width: 13ch; + font-size: 12px; +} + +.hyper-checkout-item-value { + font-size: 12px; + overflow-x: hidden; + overflow-y: auto; + word-wrap: break-word; + font-weight: 400; + text-align: center; +} + +#hyper-checkout-status-redirect-message { + margin-top: 20px; + font-family: "Montserrat"; + font-size: 13px; +} + +.ellipsis-container-2 { + height: 2.5em; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + text-overflow: ellipsis; + white-space: normal; +} + +@media only screen and (max-width: 1136px) { + .info { + flex-flow: column; + align-self: flex-start; + align-items: flex-start; + min-width: auto; + } + + .value { + margin: 0; + } +} \ No newline at end of file diff --git a/crates/router/src/core/payment_link/payment_link_status/status.html b/crates/router/src/core/payment_link/payment_link_status/status.html new file mode 100644 index 000000000000..00f1efe38c01 --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_status/status.html @@ -0,0 +1,26 @@ + + + + + Payment Status + + + + + +
+
+
+
+
+
+
+ + diff --git a/crates/router/src/core/payment_link/payment_link_status/status.js b/crates/router/src/core/payment_link/payment_link_status/status.js new file mode 100644 index 000000000000..7bf2dbaabb3b --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_status/status.js @@ -0,0 +1,379 @@ +// @ts-check + +/** + * UTIL FUNCTIONS + */ + +/** + * Ref - https://github.com/onury/invert-color/blob/master/lib/cjs/invert.js + */ +function padz(str, len) { + if (len === void 0) { + len = 2; + } + return (new Array(len).join("0") + str).slice(-len); +} +function hexToRgbArray(hex) { + if (hex.slice(0, 1) === "#") hex = hex.slice(1); + var RE_HEX = /^(?:[0-9a-f]{3}){1,2}$/i; + if (!RE_HEX.test(hex)) throw new Error('Invalid HEX color: "' + hex + '"'); + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + return [ + parseInt(hex.slice(0, 2), 16), + parseInt(hex.slice(2, 4), 16), + parseInt(hex.slice(4, 6), 16), + ]; +} +function toRgbArray(c) { + if (!c) throw new Error("Invalid color value"); + if (Array.isArray(c)) return c; + return typeof c === "string" ? hexToRgbArray(c) : [c.r, c.g, c.b]; +} +function getLuminance(c) { + var i, x; + var a = []; + for (i = 0; i < c.length; i++) { + x = c[i] / 255; + a[i] = x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); + } + return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2]; +} +function invertToBW(color, bw, asArr) { + var DEFAULT_BW = { + black: "#090302", + white: "#FFFFFC", + threshold: Math.sqrt(1.05 * 0.05) - 0.05, + }; + var options = bw === true ? DEFAULT_BW : Object.assign({}, DEFAULT_BW, bw); + return getLuminance(color) > options.threshold + ? asArr + ? hexToRgbArray(options.black) + : options.black + : asArr + ? hexToRgbArray(options.white) + : options.white; +} +function invert(color, bw) { + if (bw === void 0) { + bw = false; + } + color = toRgbArray(color); + if (bw) return invertToBW(color, bw); + return ( + "#" + + color + .map(function (c) { + return padz((255 - c).toString(16)); + }) + .join("") + ); +} + +/** + * UTIL FUNCTIONS END HERE + */ + +// @ts-ignore +{{ payment_details_js_script }} + +// @ts-ignore +window.state = { + prevHeight: window.innerHeight, + prevWidth: window.innerWidth, + isMobileView: window.innerWidth <= 1400, +}; + +/** + * Trigger - init function invoked once the script tag is loaded + * Use + * - Update document's title + * - Update document's icon + * - Render and populate document with payment details and cart + * - Initialize event listeners for updating UI on screen size changes + * - Initialize SDK + **/ +function boot() { + // @ts-ignore + var paymentDetails = window.__PAYMENT_DETAILS; + + // Attach document icon + if (paymentDetails.merchant_logo) { + var link = document.createElement("link"); + link.rel = "icon"; + link.href = paymentDetails.merchant_logo; + link.type = "image/x-icon"; + document.head.appendChild(link); + } + + // Render status details + renderStatusDetails(paymentDetails); + + // Add event listeners + initializeEventListeners(paymentDetails); +} + +/** + * Trigger - on boot + * Uses + * - Render status details + * - Header - (amount, merchant name, merchant logo) + * - Body - status with image + * - Footer - payment details (id | error code and msg, if any) + * @param {PaymentDetails} paymentDetails + **/ +function renderStatusDetails(paymentDetails) { + var status = paymentDetails.status; + + var statusDetails = { + imageSource: "", + message: "", + status: status, + amountText: "", + items: [], + }; + + // Payment details + var paymentId = createItem("Ref Id", paymentDetails.payment_id); + // @ts-ignore + statusDetails.items.push(paymentId); + + // Status specific information + switch (status) { + case "expired": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/failed.png"; + statusDetails.status = "Payment Link Expired"; + statusDetails.message = + "Sorry, this payment link has expired. Please use below reference for further investigation."; + break; + + case "succeeded": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/success.png"; + statusDetails.message = "We have successfully received your payment"; + statusDetails.status = "Paid successfully"; + statusDetails.amountText = new Date( + paymentDetails.created + ).toTimeString(); + break; + + case "processing": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/pending.png"; + statusDetails.message = + "Sorry! Your payment is taking longer than expected. Please check back again in sometime."; + statusDetails.status = "Payment Pending"; + break; + + case "failed": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/failed.png"; + statusDetails.status = "Payment Failed!"; + var errorCodeNode = createItem("Error code", paymentDetails.error_code); + var errorMessageNode = createItem( + "Error message", + paymentDetails.error_message + ); + // @ts-ignore + statusDetails.items.push(errorMessageNode, errorCodeNode); + break; + + case "cancelled": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/failed.png"; + statusDetails.status = "Payment Cancelled"; + break; + + case "requires_merchant_action": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/pending.png"; + statusDetails.status = "Payment under review"; + break; + + case "requires_capture": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/success.png"; + statusDetails.message = "We have successfully received your payment"; + statusDetails.status = "Payment Success"; + break; + + case "partially_captured": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/success.png"; + statusDetails.message = "Partial payment was captured."; + statusDetails.status = "Payment Success"; + break; + + default: + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/failed.png"; + statusDetails.status = "Something went wrong"; + // Error details + if (typeof paymentDetails.error === "object") { + var errorCodeNode = createItem("Error Code", paymentDetails.error.code); + var errorMessageNode = createItem( + "Error Message", + paymentDetails.error.message + ); + // @ts-ignore + statusDetails.items.push(errorMessageNode, errorCodeNode); + } + break; + } + + // Form header items + var amountNode = document.createElement("div"); + amountNode.className = "hyper-checkout-status-amount"; + amountNode.innerText = paymentDetails.currency + " " + paymentDetails.amount; + var merchantLogoNode = document.createElement("img"); + merchantLogoNode.className = "hyper-checkout-status-merchant-logo"; + // @ts-ignore + merchantLogoNode.src = window.__PAYMENT_DETAILS.merchant_logo; + merchantLogoNode.alt = ""; + + // Form content items + var statusImageNode = document.createElement("img"); + statusImageNode.className = "hyper-checkout-status-image"; + statusImageNode.src = statusDetails.imageSource; + var statusTextNode = document.createElement("div"); + statusTextNode.className = "hyper-checkout-status-text"; + statusTextNode.innerText = statusDetails.status; + var statusMessageNode = document.createElement("div"); + statusMessageNode.className = "hyper-checkout-status-message"; + statusMessageNode.innerText = statusDetails.message; + var statusDetailsNode = document.createElement("div"); + statusDetailsNode.className = "hyper-checkout-status-details"; + + // Append items + if (statusDetailsNode instanceof HTMLDivElement) { + statusDetails.items.map(function (item) { + statusDetailsNode.append(item); + }); + } + var statusHeaderNode = document.getElementById( + "hyper-checkout-status-header" + ); + if (statusHeaderNode instanceof HTMLDivElement) { + statusHeaderNode.append(amountNode, merchantLogoNode); + } + var statusContentNode = document.getElementById( + "hyper-checkout-status-content" + ); + if (statusContentNode instanceof HTMLDivElement) { + statusContentNode.append(statusImageNode, statusTextNode); + if (statusMessageNode instanceof HTMLDivElement) { + statusContentNode.append(statusMessageNode); + } + statusContentNode.append(statusDetailsNode); + } + + if (paymentDetails.redirect === true) { + // Form redirect text + var statusRedirectTextNode = document.getElementById( + "hyper-checkout-status-redirect-message" + ); + if ( + statusRedirectTextNode instanceof HTMLDivElement && + typeof paymentDetails.return_url === "string" + ) { + var timeout = 5, + j = 0; + for (var i = 0; i <= timeout; i++) { + setTimeout(function () { + var secondsLeft = timeout - j++; + var innerText = + secondsLeft === 0 + ? "Redirecting ..." + : "Redirecting in " + secondsLeft + " seconds ..."; + // @ts-ignore + statusRedirectTextNode.innerText = innerText; + if (secondsLeft === 0) { + // Form query params + var queryParams = { + payment_id: paymentDetails.payment_id, + status: paymentDetails.status, + }; + var url = new URL(paymentDetails.return_url); + var params = new URLSearchParams(url.search); + // Attach query params to return_url + for (var key in queryParams) { + if (queryParams.hasOwnProperty(key)) { + params.set(key, queryParams[key]); + } + } + url.search = params.toString(); + setTimeout(function () { + // Finally redirect + window.location.href = url.toString(); + }, 1000); + } + }, i * 1000); + } + } + } +} + +/** + * Use - create an item which is a key-value pair of some information related to a payment + * @param {String} heading + * @param {String} value + **/ +function createItem(heading, value) { + var itemNode = document.createElement("div"); + itemNode.className = "hyper-checkout-status-item"; + var headerNode = document.createElement("div"); + headerNode.className = "hyper-checkout-item-header"; + headerNode.innerText = heading; + var valueNode = document.createElement("div"); + valueNode.className = "hyper-checkout-item-value"; + valueNode.innerText = value; + itemNode.append(headerNode); + itemNode.append(valueNode); + return itemNode; +} + +/** + * Use - add event listeners for changing UI on screen resize + * @param {PaymentDetails} paymentDetails + */ +function initializeEventListeners(paymentDetails) { + var primaryColor = paymentDetails.theme; + var contrastBWColor = invert(primaryColor, true); + var statusRedirectTextNode = document.getElementById( + "hyper-checkout-status-redirect-message" + ); + + if (window.innerWidth <= 1400) { + if (statusRedirectTextNode instanceof HTMLDivElement) { + statusRedirectTextNode.style.color = "#333333"; + } + } else if (window.innerWidth > 1400) { + if (statusRedirectTextNode instanceof HTMLDivElement) { + statusRedirectTextNode.style.color = contrastBWColor; + } + } + + window.addEventListener("resize", function (event) { + var currentHeight = window.innerHeight; + var currentWidth = window.innerWidth; + // @ts-ignore + if (currentWidth <= 1400 && window.state.prevWidth > 1400) { + try { + if (statusRedirectTextNode instanceof HTMLDivElement) { + statusRedirectTextNode.style.color = "#333333"; + } + } catch (error) { + console.error("Failed to fetch primary-color, using default", error); + } + // @ts-ignore + } else if (currentWidth > 1400 && window.state.prevWidth <= 1400) { + try { + if (statusRedirectTextNode instanceof HTMLDivElement) { + statusRedirectTextNode.style.color = contrastBWColor; + } + } catch (error) { + console.error("Failed to revert back to default colors", error); + } + } + + // @ts-ignore + window.state.prevHeight = currentHeight; + // @ts-ignore + window.state.prevWidth = currentWidth; + // @ts-ignore + window.state.isMobileView = currentWidth <= 1400; + }); +} diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index b19b381af507..c1617fa77c99 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, @@ -11,11 +13,15 @@ use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; use diesel_models::enums; use crate::{ - core::{errors::RouterResult, payments::helpers}, + core::{ + errors::RouterResult, + payments::helpers, + pm_auth::{self as core_pm_auth}, + }, 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_token_data: Option<&CardToken>, + customer: &Option, + ) -> 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_token_data: Option<&CardToken>, + customer: &Option, + ) -> RouterResult> { + match token_data { + storage::PaymentTokenData::TemporaryGeneric(generic_token) => { + helpers::retrieve_payment_method_with_temporary_token( + state, + &generic_token.token, + payment_intent, + 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, + 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_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_token_data, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } + + storage::PaymentTokenData::AuthBankDebit(auth_token) => { + core_pm_auth::retrieve_payment_method_from_auth_service( + state, + merchant_key_store, + auth_token, + payment_intent, + customer, + ) + .await + } + } + } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index f9c666cbb954..e08dbc61f5e7 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -7,22 +7,34 @@ 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, + pm_auth::PaymentMethodAuthConfig, + surcharge_decision_configs as api_surcharge_decision_configs, }; use common_utils::{ consts, ext_traits::{AsyncExt, StringExt, ValueExt}, generate_id, }; -use diesel_models::{encryption::Encryption, enums as storage_enums, payment_method}; +use diesel_models::{ + business_profile::BusinessProfile, encryption::Encryption, enums as storage_enums, + payment_method, +}; 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, + perform_surcharge_decision_management_for_saved_cards, +}; +#[cfg(not(feature = "connector_choice_mca_id"))] +use crate::core::utils::get_connector_label; use crate::{ configs::settings, core::{ @@ -35,6 +47,7 @@ use crate::{ helpers, routing::{self, SessionFlowRoutingInput}, }, + utils as core_utils, }, db, logger, pii::prelude::*, @@ -50,7 +63,7 @@ use crate::{ self, types::{decrypt, encrypt_optional, AsyncLift}, }, - storage::{self, enums}, + storage::{self, enums, PaymentTokenData}, transformers::ForeignFrom, }, utils::{self, ConnectorResponseExt, OptionExt}, @@ -92,6 +105,29 @@ pub async fn create_payment_method( Ok(response) } +pub fn store_default_payment_method( + req: &api::PaymentMethodCreate, + customer_id: &str, + merchant_id: &String, +) -> (api::PaymentMethodResponse, bool) { + let pm_id = generate_id(consts::ID_LENGTH, "pm"); + let payment_method_response = api::PaymentMethodResponse { + merchant_id: merchant_id.to_string(), + customer_id: Some(customer_id.to_owned()), + payment_method_id: pm_id, + payment_method: req.payment_method, + payment_method_type: req.payment_method_type, + bank_transfer: None, + card: None, + metadata: req.metadata.clone(), + created: Some(common_utils::date_time::now()), + recurring_enabled: false, //[#219] + installment_payment_enabled: false, //[#219] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219] + }; + (payment_method_response, false) +} + #[instrument(skip_all)] pub async fn add_payment_method( state: routes::AppState, @@ -102,34 +138,44 @@ pub async fn add_payment_method( req.validate()?; 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"), - None => { - let pm_id = generate_id(consts::ID_LENGTH, "pm"); - let payment_method_response = api::PaymentMethodResponse { - merchant_id: merchant_id.to_string(), - customer_id: Some(customer_id.clone()), - payment_method_id: pm_id, - payment_method: req.payment_method, - payment_method_type: req.payment_method_type, - card: None, - metadata: req.metadata.clone(), - created: Some(common_utils::date_time::now()), - recurring_enabled: false, //[#219] - installment_payment_enabled: false, //[#219] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219] - }; - Ok((payment_method_response, false)) - } + + let response = match req.payment_method { + api_enums::PaymentMethod::BankTransfer => match req.bank_transfer.clone() { + Some(bank) => add_bank_to_locker( + &state, + req.clone(), + merchant_account, + key_store, + &bank, + &customer_id, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add PaymentMethod Failed"), + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), + }, + api_enums::PaymentMethod::Card => match req.card.clone() { + 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") + } + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), + }, + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), }; let (resp, is_duplicate) = response?; @@ -157,7 +203,7 @@ pub async fn add_payment_method( .await?; } - Ok(resp).map(services::ApplicationResponse::Json) + Ok(services::ApplicationResponse::Json(resp)) } #[instrument(skip_all)] @@ -190,6 +236,7 @@ pub async fn update_customer_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, + bank_transfer: req.bank_transfer, card: req.card, metadata: req.metadata, customer_id: Some(pm.customer_id), @@ -198,41 +245,171 @@ pub async fn update_customer_payment_method( .as_ref() .map(|card_network| card_network.to_string()), }; - add_payment_method(state, new_pm, &merchant_account, &key_store).await + Box::pin(add_payment_method( + state, + new_pm, + &merchant_account, + &key_store, + )) + .await } // Wrapper function to switch lockers +pub async fn add_bank_to_locker( + state: &routes::AppState, + req: api::PaymentMethodCreate, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + bank: &api::BankPayout, + customer_id: &String, +) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> { + let key = key_store.key.get_inner().peek(); + let payout_method_data = api::PayoutMethodData::Bank(bank.clone()); + let enc_data = async { + serde_json::to_value(payout_method_data.to_owned()) + .map_err(|err| { + logger::error!("Error while encoding payout method data: {}", err); + errors::VaultError::SavePaymentMethodFailed + }) + .into_report() + .change_context(errors::VaultError::SavePaymentMethodFailed) + .attach_printable("Unable to encode payout method data") + .ok() + .map(|v| { + let secret: Secret = Secret::new(v.to_string()); + secret + }) + .async_lift(|inner| encrypt_optional(inner, key)) + .await + } + .await + .change_context(errors::VaultError::SavePaymentMethodFailed) + .attach_printable("Failed to encrypt payout method data")? + .map(Encryption::from) + .map(|e| e.into_inner()) + .map_or(Err(errors::VaultError::SavePaymentMethodFailed), |e| { + Ok(hex::encode(e.peek())) + })?; + + let payload = + payment_methods::StoreLockerReq::LockerGeneric(payment_methods::StoreGenericReq { + merchant_id: &merchant_account.merchant_id, + merchant_customer_id: customer_id.to_owned(), + enc_data, + }); + let store_resp = call_to_locker_hs( + state, + &payload, + customer_id, + api_enums::LockerChoice::Basilisk, + ) + .await?; + let payment_method_resp = payment_methods::mk_add_bank_response_hs( + bank.clone(), + store_resp.card_reference, + req, + &merchant_account.merchant_id, + ); + Ok((payment_method_resp, store_resp.duplicate.unwrap_or(false))) +} + /// The response will be the tuple of PaymentMethodResponse and the duplication check of 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, + req.clone(), card, - customer_id, + customer_id.to_string(), merchant_account, api_enums::LockerChoice::Basilisk, None, ) .await .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); error }) }, &metrics::CARD_ADD_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new( + "locker", "basilisk", + )], ) - .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, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); + error + }) + }, + &metrics::CARD_ADD_TIME, + &[router_env::opentelemetry::KeyValue::new("locker", "rust")], + ) + .await; + + match add_card_to_rs_resp { + value @ Ok(_) => { + logger::debug!("card added to rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); + value + } + Err(err) => { + logger::debug!(error =? err,"failed to add card to rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); + Ok(add_card_to_hs_resp) + } + } } pub async fn get_card_from_locker( @@ -243,21 +420,91 @@ 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, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); + error + }) + }, + &metrics::CARD_GET_TIME, + &[router_env::opentelemetry::KeyValue::new("locker", "rust")], + ) + .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") .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); error }) - }, - &metrics::CARD_GET_TIME, - &[], - ) - .await + }, + &metrics::CARD_GET_TIME, + &[router_env::opentelemetry::KeyValue::new( + "locker", "basilisk", + )], + ) + .await + .map(|inner_card| { + logger::debug!("card retrieved from basilisk locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); + inner_card + }), + Ok(_) => { + logger::debug!("card retrieved from rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); + get_card_from_rs_locker_resp + } + } } pub async fn delete_card_from_locker( @@ -287,7 +534,7 @@ 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, @@ -296,7 +543,7 @@ pub async fn add_card_hs( let payload = payment_methods::StoreLockerReq::LockerCard(payment_methods::StoreCardReq { merchant_id: &merchant_account.merchant_id, merchant_customer_id: customer_id.to_owned(), - card_reference: card_reference.map(str::to_string), + 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(), @@ -307,15 +554,17 @@ 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, 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, ); + Ok(( payment_method_resp, store_card_payload.duplicate.unwrap_or(false), @@ -351,6 +600,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"))] @@ -365,6 +615,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) @@ -376,10 +627,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)?; @@ -426,10 +678,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)?; @@ -466,6 +719,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"))] @@ -480,6 +734,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) @@ -491,10 +746,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)?; @@ -543,10 +799,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)?; @@ -924,6 +1184,12 @@ pub async fn list_payment_methods( }) .await .transpose()?; + let business_profile = core_utils::validate_and_get_business_profile( + db, + profile_id.as_ref(), + &merchant_account.merchant_id, + ) + .await?; // filter out connectors based on the business country let filtered_mcas = helpers::filter_mca_based_on_business_profile(all_mcas, profile_id); @@ -931,9 +1197,9 @@ pub async fn list_payment_methods( logger::debug!(mca_before_filtering=?filtered_mcas); let mut response: Vec = vec![]; - for mca in filtered_mcas { - let payment_methods = match mca.payment_methods_enabled { - Some(pm) => pm, + for mca in &filtered_mcas { + let payment_methods = match &mca.payment_methods_enabled { + Some(pm) => pm.clone(), None => continue, }; @@ -944,13 +1210,15 @@ pub async fn list_payment_methods( payment_intent.as_ref(), payment_attempt.as_ref(), billing_address.as_ref(), - mca.connector_name, + mca.connector_name.clone(), pm_config_mapping, &state.conf.mandates.supported_payment_methods, ) .await?; } + let mut pmt_to_auth_connector = HashMap::new(); + if let Some((payment_attempt, payment_intent)) = payment_attempt.as_ref().zip(payment_intent.as_ref()) { @@ -1054,6 +1322,84 @@ pub async fn list_payment_methods( pre_routing_results.insert(pm_type, routable_choice); } + let redis_conn = db + .get_redis_conn() + .map_err(|redis_error| logger::error!(?redis_error)) + .ok(); + + let mut val = Vec::new(); + + for (payment_method_type, routable_connector_choice) in &pre_routing_results { + #[cfg(not(feature = "connector_choice_mca_id"))] + let connector_label = get_connector_label( + payment_intent.business_country, + payment_intent.business_label.as_ref(), + #[cfg(not(feature = "connector_choice_mca_id"))] + routable_connector_choice.sub_label.as_ref(), + #[cfg(feature = "connector_choice_mca_id")] + None, + routable_connector_choice.connector.to_string().as_str(), + ); + #[cfg(not(feature = "connector_choice_mca_id"))] + let matched_mca = filtered_mcas + .iter() + .find(|m| connector_label == m.connector_label); + + #[cfg(feature = "connector_choice_mca_id")] + let matched_mca = filtered_mcas.iter().find(|m| { + routable_connector_choice.merchant_connector_id.as_ref() + == Some(&m.merchant_connector_id) + }); + + if let Some(m) = matched_mca { + let pm_auth_config = m + .pm_auth_config + .as_ref() + .map(|config| { + serde_json::from_value::(config.clone()) + .into_report() + .change_context(errors::StorageError::DeserializationFailed) + .attach_printable("Failed to deserialize Payment Method Auth config") + }) + .transpose() + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + + let matched_config = match pm_auth_config { + Some(config) => { + let internal_config = config + .enabled_payment_methods + .iter() + .find(|config| config.payment_method_type == *payment_method_type) + .cloned(); + + internal_config + } + None => None, + }; + + if let Some(config) = matched_config { + pmt_to_auth_connector + .insert(*payment_method_type, config.connector_name.clone()); + val.push(config); + } + } + } + + let pm_auth_key = format!("pm_auth_{}", payment_intent.payment_id); + let redis_expiry = state.conf.payment_method_auth.redis_expiry; + + if let Some(rc) = redis_conn { + rc.serialize_and_set_key_with_expiry(pm_auth_key.as_str(), val, redis_expiry) + .await + .attach_printable("Failed to store pm auth data in redis") + .unwrap_or_else(|err| { + logger::error!(error=?err); + }) + }; + routing_info.pre_routing_results = Some(pre_routing_results); let encoded = utils::Encode::::encode_to_value(&routing_info) @@ -1311,6 +1657,9 @@ 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: pmt_to_auth_connector + .get(payment_method_types_hm.0) + .cloned(), }) } @@ -1345,6 +1694,9 @@ 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: pmt_to_auth_connector + .get(payment_method_types_hm.0) + .cloned(), }) } @@ -1374,6 +1726,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: pmt_to_auth_connector.get(&payment_method_type).cloned(), } }) } @@ -1406,6 +1759,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: pmt_to_auth_connector.get(&payment_method_type).cloned(), } }) } @@ -1438,6 +1792,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: pmt_to_auth_connector.get(&payment_method_type).cloned(), } }) } @@ -1448,15 +1803,44 @@ pub async fn list_payment_methods( payment_method_types: bank_transfer_payment_method_types, }); } - + let merchant_surcharge_configs = + if let Some((payment_attempt, payment_intent, business_profile)) = payment_attempt + .as_ref() + .zip(payment_intent) + .zip(business_profile) + .map(|((pa, pi), bp)| (pa, pi, bp)) + { + Box::pin(call_surcharge_decision_management( + state, + &merchant_account, + &business_profile, + payment_attempt, + payment_intent, + billing_address, + &mut payment_method_responses, + )) + .await? + } else { + api_surcharge_decision_configs::MerchantSurchargeConfigs::default() + }; + print!("PAMT{:?}", payment_attempt); Ok(services::ApplicationResponse::Json( api::PaymentMethodListResponse { redirect_url: merchant_account.return_url, merchant_name: merchant_account.merchant_name, payment_type, payment_methods: payment_method_responses, - mandate_payment: payment_attempt.and_then(|inner| inner.mandate_details).map( - |d| match d { + mandate_payment: payment_attempt + .and_then(|inner| inner.mandate_details) + .and_then(|man_type_details| match man_type_details { + data_models::mandates::MandateTypeDetails::MandateType(mandate_type) => { + Some(mandate_type) + } + data_models::mandates::MandateTypeDetails::MandateDetails(mandate_details) => { + mandate_details.mandate_type + } + }) + .map(|d| match d { data_models::mandates::MandateDataType::SingleUse(i) => { api::MandateType::SingleUse(api::MandateAmountData { amount: i.amount, @@ -1478,13 +1862,111 @@ pub async fn list_payment_methods( data_models::mandates::MandateDataType::MultiUse(None) => { api::MandateType::MultiUse(None) } - }, - ), - 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, + business_profile: &BusinessProfile, + payment_attempt: &storage::PaymentAttempt, + payment_intent: storage::PaymentIntent, + billing_address: Option, + response_payment_method_types: &mut [ResponsePaymentMethodsEnabled], +) -> errors::RouterResult { + 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() { + surcharge_results + .persist_individual_surcharge_details_in_redis(&state, business_profile) + .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) +} + +pub async fn call_surcharge_decision_management_for_saved_card( + state: &routes::AppState, + merchant_account: &domain::MerchantAccount, + business_profile: &BusinessProfile, + payment_attempt: &storage::PaymentAttempt, + payment_intent: storage::PaymentIntent, + customer_payment_method_response: &mut api::CustomerPaymentMethodsListResponse, +) -> errors::RouterResult<()> { + 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 = perform_surcharge_decision_management_for_saved_cards( + state, + algorithm_ref, + payment_attempt, + &payment_intent, + &mut customer_payment_method_response.customer_payment_methods, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + if !surcharge_results.is_empty_result() { + surcharge_results + .persist_individual_surcharge_details_in_redis(state, business_profile) + .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(()) +} + #[allow(clippy::too_many_arguments)] pub async fn filter_payment_methods( payment_methods: Vec, @@ -1875,7 +2357,7 @@ fn filter_amount_based(payment_method: &RequestPaymentMethodTypes, amount: Optio // (Some(amt), Some(max_amt)) => amt <= max_amt, // (_, _) => true, // }; - min_check && max_check + (min_check && max_check) || amount == Some(0) } fn filter_pm_based_on_allowed_types( @@ -1934,8 +2416,9 @@ fn filter_payment_amount_based( pm: &RequestPaymentMethodTypes, ) -> bool { let amount = payment_intent.amount; - pm.maximum_amount.map_or(true, |amt| amount < amt.into()) - && pm.minimum_amount.map_or(true, |amt| amount > amt.into()) + (pm.maximum_amount.map_or(true, |amt| amount <= amt.into()) + && pm.minimum_amount.map_or(true, |amt| amount >= amt.into())) + || payment_intent.amount == 0 } async fn filter_payment_mandate_based( @@ -1969,12 +2452,13 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( .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( - db, - &merchant_account, - cloned_secret, - ) - .await?; + let payment_intent: Option = + helpers::verify_payment_intent_time_and_client_secret( + db, + &merchant_account, + cloned_secret, + ) + .await?; let customer_id = payment_intent .as_ref() .and_then(|intent| intent.customer_id.to_owned()) @@ -2030,24 +2514,80 @@ 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 => { + let card_details = get_card_details_with_locker_fallback(&pm, key, state).await?; + + if card_details.is_some() { + ( + card_details, + None, + PaymentTokenData::permanent_card(pm.payment_method_id.clone()), + ) + } else { + continue; + } + } + + #[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_bank_from_hs_locker( + state, + &key_store, + &token, + &pm.customer_id, + &pm.customer_id, + &pm.payment_method_id, + ) + .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(), customer_id: pm.customer_id, @@ -2061,10 +2601,9 @@ 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, + surcharge_details: None, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2080,7 +2619,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 { @@ -2116,21 +2655,74 @@ pub async fn list_customer_payment_method( } } - let response = api::CustomerPaymentMethodsListResponse { + let mut response = api::CustomerPaymentMethodsListResponse { customer_payment_methods: customer_pms, }; + let payment_attempt = payment_intent + .as_ref() + .async_map(|payment_intent| async { + state + .store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_intent.payment_id, + &merchant_account.merchant_id, + &payment_intent.active_attempt.get_id(), + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + }) + .await + .transpose()?; + + let profile_id = payment_intent + .as_ref() + .async_map(|payment_intent| async { + crate::core::utils::get_profile_id_from_business_details( + payment_intent.business_country, + payment_intent.business_label.as_ref(), + &merchant_account, + payment_intent.profile_id.as_ref(), + db, + false, + ) + .await + .attach_printable("Could not find profile id from business details") + }) + .await + .transpose()?; + let business_profile = core_utils::validate_and_get_business_profile( + db, + profile_id.as_ref(), + &merchant_account.merchant_id, + ) + .await?; + + if let Some((payment_attempt, payment_intent, business_profile)) = payment_attempt + .zip(payment_intent) + .zip(business_profile) + .map(|((pa, pi), bp)| (pa, pi, bp)) + { + call_surcharge_decision_management_for_saved_card( + state, + &merchant_account, + &business_profile, + &payment_attempt, + payment_intent, + &mut response, + ) + .await?; + } Ok(services::ApplicationResponse::Json(response)) } -async fn get_card_details( +pub async fn get_card_details_with_locker_fallback( pm: &payment_method::PaymentMethod, key: &[u8], state: &routes::AppState, - hyperswitch_token: &str, - key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult> { - let mut _card_decrypted = + let card_decrypted = decrypt::(pm.payment_method_data.clone(), key) .await .change_context(errors::StorageError::DecryptionError) @@ -2144,16 +2736,48 @@ 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 { + if crd.saved_to_locker { + crd.scheme = pm.scheme.clone(); + Some(crd) + } else { + None + } + } else { + Some(get_card_details_from_locker(state, pm).await?) + }) } -pub async fn get_lookup_key_from_locker( +pub async fn get_card_details_without_locker_fallback( + pm: &payment_method::PaymentMethod, + key: &[u8], + state: &routes::AppState, +) -> errors::RouterResult { + let card_decrypted = + decrypt::(pm.payment_method_data.clone(), key) + .await + .change_context(errors::StorageError::DecryptionError) + .attach_printable("unable to decrypt card details") + .ok() + .flatten() + .map(|x| x.into_inner().expose()) + .and_then(|v| serde_json::from_value::(v).ok()) + .and_then(|pmd| match pmd { + PaymentMethodsData::Card(crd) => Some(api::CardDetailFromLocker::from(crd)), + _ => None, + }); + + 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_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, @@ -2164,9 +2788,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( @@ -2180,19 +2814,99 @@ 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( +pub async fn get_bank_from_hs_locker( state: &routes::AppState, key_store: &domain::MerchantKeyStore, - payout_token: &str, - pm: &storage::PaymentMethod, + temp_token: &str, + customer_id: &str, + merchant_id: &str, + token_ref: &str, ) -> errors::RouterResult { let payment_method = get_payment_method_from_hs_locker( state, key_store, - &pm.customer_id, - &pm.merchant_id, - &pm.payment_method_id, + customer_id, + merchant_id, + token_ref, + None, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -2206,9 +2920,9 @@ pub async fn get_lookup_key_for_payout_method( api::PayoutMethodData::Bank(bank) => { vault::Vault::store_payout_method_data_in_locker( state, - Some(payout_token.to_string()), + Some(temp_token.to_string()), &pm_parsed, - Some(pm.customer_id.to_owned()), + Some(customer_id.to_owned()), key_store, ) .await @@ -2297,6 +3011,14 @@ impl TempLockerCardSupport { ) .await?; metrics::TOKENIZED_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); + metrics::TASKS_ADDED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes( + "flow", + "DeleteTokenizeData", + )], + ); Ok(card) } } @@ -2305,25 +3027,32 @@ impl TempLockerCardSupport { pub async fn retrieve_payment_method( state: routes::AppState, pm: api::PaymentMethodId, + key_store: domain::MerchantKeyStore, ) -> errors::RouterResponse { let db = state.store.as_ref(); let pm = db .find_payment_method(&pm.payment_method_id) .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + let key = key_store.key.peek(); let card = if pm.payment_method == enums::PaymentMethod::Card { - let card = get_card_from_locker( - &state, - &pm.customer_id, - &pm.merchant_id, - &pm.payment_method_id, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting card from card vault")?; - let card_detail = payment_methods::get_card_detail(&pm, card) + let card_detail = if state.conf.locker.locker_enabled { + let card = get_card_from_locker( + &state, + &pm.customer_id, + &pm.merchant_id, + &pm.payment_method_id, + ) + .await .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting card details from locker")?; + .attach_printable("Error getting card from card vault")?; + payment_methods::get_card_detail(&pm, card) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting card details from locker")? + } else { + get_card_details_without_locker_fallback(&pm, key, &state).await? + }; Some(card_detail) } else { None @@ -2335,6 +3064,7 @@ pub async fn retrieve_payment_method( payment_method_id: pm.payment_method_id, payment_method: pm.payment_method, payment_method_type: pm.payment_method_type, + bank_transfer: None, card, metadata: pm.metadata, created: Some(pm.created_at), 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..f13c674eca72 --- /dev/null +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -0,0 +1,456 @@ +use std::sync::Arc; + +use api_models::{ + payment_methods::SurchargeDetailsResponse, + payments, routing, + surcharge_decision_configs::{self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord}, +}; +use common_utils::{ext_traits::StringExt, static_cache::StaticCache, types as common_utils_types}; +use error_stack::{self, IntoReport, ResultExt}; +use euclid::{ + backend, + backend::{inputs as dsl_inputs, EuclidBackend}, +}; +use router_env::{instrument, tracing}; + +use crate::{ + core::payments::{types, PaymentData}, + db::StorageInterface, + types::{ + storage::{self as oss_storage, payment_attempt::PaymentAttemptExt}, + transformers::ForeignTryFrom, + }, +}; +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, + }) + } +} + +enum SurchargeSource { + /// Surcharge will be generated through the surcharge rules + Generate(Arc), + /// Surcharge is predefined by the merchant through payment create request + Predetermined(payments::RequestSurchargeDetails), +} + +impl SurchargeSource { + pub fn generate_surcharge_details_and_populate_surcharge_metadata( + &self, + backend_input: &backend::BackendInput, + payment_attempt: &oss_storage::PaymentAttempt, + surcharge_metadata_and_key: (&mut types::SurchargeMetadata, types::SurchargeKey), + ) -> ConditionalConfigResult> { + match self { + Self::Generate(interpreter) => { + let surcharge_output = execute_dsl_and_get_conditional_config( + backend_input.clone(), + &interpreter.cached_alogorith, + )?; + Ok(surcharge_output + .surcharge_details + .map(|surcharge_details| { + get_surcharge_details_from_surcharge_output( + surcharge_details, + payment_attempt, + ) + }) + .transpose()? + .map(|surcharge_details| { + let (surcharge_metadata, surcharge_key) = surcharge_metadata_and_key; + surcharge_metadata + .insert_surcharge_details(surcharge_key, surcharge_details.clone()); + surcharge_details + })) + } + Self::Predetermined(request_surcharge_details) => Ok(Some( + types::SurchargeDetails::from((request_surcharge_details, payment_attempt)), + )), + } + } +} + +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<( + types::SurchargeMetadata, + surcharge_decision_configs::MerchantSurchargeConfigs, +)> { + let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + + let (surcharge_source, merchant_surcharge_configs) = match ( + payment_attempt.get_surcharge_details(), + algorithm_ref.surcharge_config_algo_id, + ) { + (Some(request_surcharge_details), _) => ( + SurchargeSource::Predetermined(request_surcharge_details), + surcharge_decision_configs::MerchantSurchargeConfigs::default(), + ), + (None, Some(algorithm_id)) => { + 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 merchant_surcharge_config = cached_algo.merchant_surcharge_configs.clone(); + ( + SurchargeSource::Generate(cached_algo), + merchant_surcharge_config, + ) + } + (None, None) => { + return Ok(( + surcharge_metadata, + surcharge_decision_configs::MerchantSurchargeConfigs::default(), + )) + } + }; + + let mut backend_input = + make_dsl_input_for_surcharge(payment_attempt, payment_intent, billing_address) + .change_context(ConfigError::InputConstructionError)?; + + 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_details = surcharge_source + .generate_surcharge_details_and_populate_surcharge_metadata( + &backend_input, + payment_attempt, + ( + &mut surcharge_metadata, + types::SurchargeKey::PaymentMethodData( + payment_methods_enabled.payment_method, + payment_method_type_response.payment_method_type, + Some(card_network_type.card_network.clone()), + ), + ), + )?; + card_network_type.surcharge_details = surcharge_details + .map(|surcharge_details| { + SurchargeDetailsResponse::foreign_try_from(( + &surcharge_details, + payment_attempt, + )) + .into_report() + .change_context(ConfigError::DslExecutionError) + .attach_printable("Error while constructing Surcharge response type") + }) + .transpose()?; + } + } else { + let surcharge_details = surcharge_source + .generate_surcharge_details_and_populate_surcharge_metadata( + &backend_input, + payment_attempt, + ( + &mut surcharge_metadata, + types::SurchargeKey::PaymentMethodData( + payment_methods_enabled.payment_method, + payment_method_type_response.payment_method_type, + None, + ), + ), + )?; + payment_method_type_response.surcharge_details = surcharge_details + .map(|surcharge_details| { + SurchargeDetailsResponse::foreign_try_from(( + &surcharge_details, + payment_attempt, + )) + .into_report() + .change_context(ConfigError::DslExecutionError) + .attach_printable("Error while constructing Surcharge response type") + }) + .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 = + types::SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); + let surcharge_source = match ( + payment_data.payment_attempt.get_surcharge_details(), + algorithm_ref.surcharge_config_algo_id, + ) { + (Some(request_surcharge_details), _) => { + SurchargeSource::Predetermined(request_surcharge_details) + } + (None, Some(algorithm_id)) => { + 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", + )?; + SurchargeSource::Generate(cached_algo) + } + (None, None) => return Ok(surcharge_metadata), + }; + 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)?; + 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()); + surcharge_source.generate_surcharge_details_and_populate_surcharge_metadata( + &backend_input, + &payment_data.payment_attempt, + ( + &mut surcharge_metadata, + types::SurchargeKey::PaymentMethodData( + payment_method_type.to_owned().into(), + *payment_method_type, + None, + ), + ), + )?; + } + Ok(surcharge_metadata) +} +pub async fn perform_surcharge_decision_management_for_saved_cards( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + customer_payment_method_list: &mut [api_models::payment_methods::CustomerPaymentMethod], +) -> ConditionalConfigResult { + let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let surcharge_source = match ( + payment_attempt.get_surcharge_details(), + algorithm_ref.surcharge_config_algo_id, + ) { + (Some(request_surcharge_details), _) => { + SurchargeSource::Predetermined(request_surcharge_details) + } + (None, Some(algorithm_id)) => { + 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", + )?; + SurchargeSource::Generate(cached_algo) + } + (None, None) => return Ok(surcharge_metadata), + }; + let mut backend_input = make_dsl_input_for_surcharge(payment_attempt, payment_intent, None) + .change_context(ConfigError::InputConstructionError)?; + + for customer_payment_method in customer_payment_method_list.iter_mut() { + backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method); + backend_input.payment_method.payment_method_type = + customer_payment_method.payment_method_type; + backend_input.payment_method.card_network = customer_payment_method + .card + .as_ref() + .and_then(|card| card.scheme.as_ref()) + .map(|scheme| { + scheme + .clone() + .parse_enum("CardNetwork") + .change_context(ConfigError::DslExecutionError) + }) + .transpose()?; + let surcharge_details = surcharge_source + .generate_surcharge_details_and_populate_surcharge_metadata( + &backend_input, + payment_attempt, + ( + &mut surcharge_metadata, + types::SurchargeKey::Token(customer_payment_method.payment_token.clone()), + ), + )?; + customer_payment_method.surcharge_details = surcharge_details + .map(|surcharge_details| { + SurchargeDetailsResponse::foreign_try_from((&surcharge_details, payment_attempt)) + .into_report() + .change_context(ConfigError::DslParsingError) + }) + .transpose()?; + } + Ok(surcharge_metadata) +} + +fn get_surcharge_details_from_surcharge_output( + surcharge_details: surcharge_decision_configs::SurchargeDetailsOutput, + payment_attempt: &oss_storage::PaymentAttempt, +) -> ConditionalConfigResult { + let surcharge_amount = match surcharge_details.surcharge.clone() { + surcharge_decision_configs::SurchargeOutput::Fixed { amount } => amount, + surcharge_decision_configs::SurchargeOutput::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(types::SurchargeDetails { + original_amount: payment_attempt.amount, + surcharge: match surcharge_details.surcharge { + surcharge_decision_configs::SurchargeOutput::Fixed { amount } => { + common_utils_types::Surcharge::Fixed(amount) + } + surcharge_decision_configs::SurchargeOutput::Rate(percentage) => { + common_utils_types::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 63a0479375e8..304091e42ac7 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use api_models::enums as api_enums; -use common_utils::{ext_traits::StringExt, pii::Email}; +use common_utils::{ext_traits::StringExt, pii::Email, request::RequestContent}; use error_stack::ResultExt; use josekit::jwe; use serde::{Deserialize, Serialize}; @@ -28,7 +28,7 @@ pub struct StoreCardReq<'a> { pub merchant_id: &'a str, pub merchant_customer_id: String, #[serde(skip_serializing_if = "Option::is_none")] - pub card_reference: Option, + pub requestor_card_reference: Option, pub card: Card, } @@ -97,7 +97,7 @@ pub struct DeleteCardResp { #[serde(rename_all = "camelCase")] pub struct AddCardRequest<'a> { pub card_number: cards::CardNumber, - pub customer_id: &'a str, + pub customer_id: String, pub card_exp_month: Secret, pub card_exp_year: Secret, pub merchant_id: &'a str, @@ -189,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(); @@ -300,9 +313,6 @@ pub async fn mk_add_locker_request_hs<'a>( .change_context(errors::VaultError::RequestEncodingFailed)?; 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 = match locker_choice { api_enums::LockerChoice::Basilisk => locker.host.to_owned(), api_enums::LockerChoice::Tartarus => locker.host_rs.to_owned(), @@ -310,28 +320,58 @@ pub async fn mk_add_locker_request_hs<'a>( url.push_str("/cards/add"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(body.to_string()); + request.set_body(RequestContent::Json(Box::new(jwe_payload))); Ok(request) } +pub fn mk_add_bank_response_hs( + bank: api::BankPayout, + bank_reference: String, + req: api::PaymentMethodCreate, + merchant_id: &str, +) -> api::PaymentMethodResponse { + api::PaymentMethodResponse { + merchant_id: merchant_id.to_owned(), + customer_id: req.customer_id, + payment_method_id: bank_reference, + payment_method: req.payment_method, + payment_method_type: req.payment_method_type, + bank_transfer: Some(bank), + card: None, + metadata: req.metadata, + created: Some(common_utils::date_time::now()), + recurring_enabled: false, // [#256] + installment_payment_enabled: false, // #[#256] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), // [#256] + } +} + pub fn mk_add_card_response_hs( card: api::CardDetail, card_reference: String, req: api::PaymentMethodCreate, merchant_id: &str, ) -> api::PaymentMethodResponse { - let mut card_number = card.card_number.peek().to_owned(); + let card_number = card.card_number.clone(); + let last4_digits = card_number.clone().get_last4(); + let card_isin = card_number.get_card_isin(); + let card = api::CardDetailFromLocker { scheme: None, - last4_digits: Some(card_number.split_off(card_number.len() - 4)), - issuer_country: None, // [#256] bin mapping - card_number: Some(card.card_number), - expiry_month: Some(card.card_exp_month), - expiry_year: Some(card.card_exp_year), - card_token: None, // [#256] - card_fingerprint: None, // fingerprint not send by basilisk-hs need to have this feature in case we need it in future - card_holder_name: card.card_holder_name, - nick_name: card.nick_name, + last4_digits: Some(last4_digits), + issuer_country: None, + card_number: Some(card.card_number.clone()), + expiry_month: Some(card.card_exp_month.clone()), + expiry_year: Some(card.card_exp_year.clone()), + card_token: None, + card_fingerprint: None, + card_holder_name: card.card_holder_name.clone(), + nick_name: card.nick_name.clone(), + card_isin: Some(card_isin), + card_issuer: card.card_issuer, + card_network: card.card_network, + card_type: card.card_type, + saved_to_locker: true, }; api::PaymentMethodResponse { merchant_id: merchant_id.to_owned(), @@ -339,6 +379,7 @@ pub fn mk_add_card_response_hs( payment_method_id: card_reference, payment_method: req.payment_method, payment_method_type: req.payment_method_type, + bank_transfer: None, card: Some(card), metadata: req.metadata, created: Some(common_utils::date_time::now()), @@ -366,6 +407,11 @@ pub fn mk_add_card_response( card_fingerprint: Some(response.card_fingerprint), card_holder_name: card.card_holder_name, nick_name: card.nick_name, + card_isin: None, + card_issuer: None, + card_network: None, + card_type: None, + saved_to_locker: true, }; api::PaymentMethodResponse { merchant_id: merchant_id.to_owned(), @@ -373,6 +419,7 @@ pub fn mk_add_card_response( payment_method_id: response.card_id, payment_method: req.payment_method, payment_method_type: req.payment_method_type, + bank_transfer: None, card: Some(card), metadata: req.metadata, created: Some(common_utils::date_time::now()), @@ -387,7 +434,7 @@ pub fn mk_add_card_request( card: &api::CardDetail, customer_id: &str, _req: &api::PaymentMethodCreate, - locker_id: &str, + locker_id: &'static str, merchant_id: &str, ) -> CustomResult { let customer_id = if cfg!(feature = "release") { @@ -397,7 +444,7 @@ pub fn mk_add_card_request( }; let add_card_req = AddCardRequest { card_number: card.card_number.clone(), - customer_id: &customer_id, + customer_id, card_exp_month: card.card_exp_month.clone(), card_exp_year: card.card_exp_year.clone(), merchant_id: locker_id, @@ -408,16 +455,10 @@ pub fn mk_add_card_request( name_on_card: Some("John Doe".to_string().into()), // [#256] nickname: Some("router".to_string()), // }; - let body = utils::Encode::>::url_encode(&add_card_req) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.host.to_owned(); url.push_str("/card/addCard"); let mut request = services::Request::new(services::Method::Post, &url); - request.add_header( - headers::CONTENT_TYPE, - "application/x-www-form-urlencoded".into(), - ); - request.set_body(body); + request.set_body(RequestContent::FormUrlEncoded(Box::new(add_card_req))); Ok(request) } @@ -428,6 +469,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 { @@ -448,38 +490,34 @@ pub async fn mk_get_card_request_hs( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?; + let target_locker = locker_choice.unwrap_or(api_enums::LockerChoice::Basilisk); - let body = utils::Encode::::encode_to_value(&jwe_payload) - .change_context(errors::VaultError::RequestEncodingFailed)?; - let mut url = locker.host.to_owned(); + let jwe_payload = mk_basilisk_req(jwekey, &jws, target_locker).await?; + 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()); - request.set_body(body.to_string()); + request.set_body(RequestContent::Json(Box::new(jwe_payload))); Ok(request) } -pub fn mk_get_card_request<'a>( +pub fn mk_get_card_request( locker: &settings::Locker, - locker_id: &'a str, - card_id: &'a str, + locker_id: &'static str, + card_id: &'static str, ) -> CustomResult { let get_card_req = GetCard { merchant_id: locker_id, card_id, }; - let body = utils::Encode::>::url_encode(&get_card_req) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.host.to_owned(); url.push_str("/card/getCard"); let mut request = services::Request::new(services::Method::Post, &url); - request.add_header( - headers::CONTENT_TYPE, - "application/x-www-form-urlencoded".into(), - ); - request.set_body(body); + request.set_body(RequestContent::FormUrlEncoded(Box::new(get_card_req))); Ok(request) } @@ -530,37 +568,29 @@ pub async fn mk_delete_card_request_hs( 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)?; let mut url = locker.host.to_owned(); url.push_str("/cards/delete"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(body.to_string()); + request.set_body(RequestContent::Json(Box::new(jwe_payload))); Ok(request) } -pub fn mk_delete_card_request<'a>( +pub fn mk_delete_card_request( locker: &settings::Locker, - merchant_id: &'a str, - card_id: &'a str, + merchant_id: &'static str, + card_id: &'static str, ) -> CustomResult { let delete_card_req = GetCard { merchant_id, card_id, }; - let body = utils::Encode::>::url_encode(&delete_card_req) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.host.to_owned(); url.push_str("/card/deleteCard"); let mut request = services::Request::new(services::Method::Post, &url); request.add_default_headers(); - request.add_header( - headers::CONTENT_TYPE, - "application/x-www-form-urlencoded".into(), - ); - request.set_body(body); + request.set_body(RequestContent::FormUrlEncoded(Box::new(delete_card_req))); Ok(request) } @@ -580,6 +610,8 @@ pub fn get_card_detail( ) -> CustomResult { let card_number = response.card_number; let mut last4_digits = card_number.peek().to_owned(); + //fetch form card bin + let card_detail = api::CardDetailFromLocker { scheme: pm.scheme.to_owned(), issuer_country: pm.issuer_country.clone(), @@ -591,6 +623,11 @@ pub fn get_card_detail( card_fingerprint: None, card_holder_name: response.name_on_card, nick_name: response.nick_name.map(masking::Secret::new), + card_isin: None, + card_issuer: None, + card_network: None, + card_type: None, + saved_to_locker: true, }; Ok(card_detail) } @@ -601,14 +638,12 @@ pub fn mk_crud_locker_request( path: &str, req: api::TokenizePayloadEncrypted, ) -> CustomResult { - let body = utils::Encode::::encode_to_value(&req) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.basilisk_host.to_owned(); url.push_str(path); let mut request = services::Request::new(services::Method::Post, &url); request.add_default_headers(); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(body.to_string()); + request.set_body(RequestContent::Json(Box::new(req))); Ok(request) } diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 5ad78c9d730e..5b783f1c5d4e 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -4,8 +4,6 @@ use common_utils::{ generate_id_with_default_len, }; use error_stack::{report, IntoReport, ResultExt}; -#[cfg(feature = "basilisk")] -use josekit::jwe; use masking::PeekInterface; use router_env::{instrument, tracing}; use scheduler::{types::process_data, utils as process_tracker_utils}; @@ -23,11 +21,7 @@ use crate::{ }, utils::{self, StringExt}, }; -#[cfg(feature = "basilisk")] -use crate::{core::payment_methods::transformers as payment_methods, services, settings}; const VAULT_SERVICE_NAME: &str = "CARD"; -#[cfg(feature = "basilisk")] -const VAULT_VERSION: &str = "0"; pub struct SupplementaryVaultData { pub customer_id: Option, @@ -51,7 +45,10 @@ impl Vaultable for api::Card { card_number: self.card_number.peek().clone(), exp_year: self.card_exp_year.peek().clone(), exp_month: self.card_exp_month.peek().clone(), - name_on_card: Some(self.card_holder_name.peek().clone()), + name_on_card: self + .card_holder_name + .as_ref() + .map(|name| name.peek().clone()), nickname: None, card_last_four: None, card_token: None, @@ -99,7 +96,7 @@ impl Vaultable for api::Card { .attach_printable("Invalid card number format from the mock locker")?, card_exp_month: value1.exp_month.into(), card_exp_year: value1.exp_year.into(), - card_holder_name: value1.name_on_card.unwrap_or_default().into(), + card_holder_name: value1.name_on_card.map(masking::Secret::new), card_cvc: value2.card_security_code.unwrap_or_default().into(), card_issuer: None, card_network: None, @@ -354,7 +351,7 @@ impl Vaultable for api::CardPayout { card_number: self.card_number.peek().clone(), exp_year: self.expiry_year.peek().clone(), exp_month: self.expiry_month.peek().clone(), - name_on_card: Some(self.card_holder_name.peek().clone()), + name_on_card: self.card_holder_name.clone().map(|n| n.peek().to_string()), nickname: None, card_last_four: None, card_token: None, @@ -400,7 +397,7 @@ impl Vaultable for api::CardPayout { .map_err(|_| errors::VaultError::FetchCardFailed)?, expiry_month: value1.exp_month.into(), expiry_year: value1.exp_year.into(), - card_holder_name: value1.name_on_card.unwrap_or_default().into(), + card_holder_name: value1.name_on_card.map(masking::Secret::new), }; let supp_data = SupplementaryVaultData { @@ -424,9 +421,9 @@ pub struct TokenizedBankSensitiveValues { #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct TokenizedBankInsensitiveValues { pub customer_id: Option, - pub bank_name: String, - pub bank_country_code: api::enums::CountryAlpha2, - pub bank_city: String, + pub bank_name: Option, + pub bank_country_code: Option, + pub bank_city: Option, } #[cfg(feature = "payouts")] @@ -705,7 +702,8 @@ impl Vault { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting Value2 for locker")?; - let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); + let lookup_key = + token_id.unwrap_or_else(|| generate_id_with_default_len("temporary_token")); let lookup_key = create_tokenize( state, @@ -803,11 +801,6 @@ pub async fn create_tokenize( } Err(err) => { logger::error!("Redis Temp locker Failed: {:?}", err); - - #[cfg(feature = "basilisk")] - return old_create_tokenize(state, value1, value2, lookup_key).await; - - #[cfg(not(feature = "basilisk"))] Err(err) } } @@ -871,11 +864,6 @@ pub async fn get_tokenized_data( } Err(err) => { logger::error!("Redis Temp locker Failed: {:?}", err); - - #[cfg(feature = "basilisk")] - return old_get_tokenized_data(state, lookup_key, _should_get_value2).await; - - #[cfg(not(feature = "basilisk"))] Err(err) } } @@ -922,11 +910,6 @@ pub async fn delete_tokenized_data(state: &routes::AppState, lookup_key: &str) - } Err(err) => { logger::error!("Redis Temp locker Failed: {:?}", err); - - #[cfg(feature = "basilisk")] - return old_delete_tokenized_data(state, lookup_key).await; - - #[cfg(not(feature = "basilisk"))] Err(err) } } @@ -1044,7 +1027,18 @@ pub async fn retry_delete_tokenize( let schedule_time = get_delete_tokenize_schedule_time(db, pm, pt.retry_count).await; match schedule_time { - Some(s_time) => pt.retry(db.as_scheduler(), s_time).await, + Some(s_time) => { + let retry_schedule = pt.retry(db.as_scheduler(), s_time).await; + metrics::TASKS_RESET_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes( + "flow", + "DeleteTokenizeData", + )], + ); + retry_schedule + } None => { pt.finish_with_status(db.as_scheduler(), "RETRIES_EXCEEDED".to_string()) .await @@ -1053,246 +1047,3 @@ pub async fn retry_delete_tokenize( } // Fallback logic of old temp locker needs to be removed later - -#[cfg(feature = "basilisk")] -async fn get_locker_jwe_keys( - keys: &settings::ActiveKmsSecrets, -) -> CustomResult<(String, String), errors::EncryptionError> { - let keys = keys.jwekey.peek(); - let key_id = get_key_id(keys); - let (public_key, private_key) = if key_id == keys.locker_key_identifier1 { - (&keys.locker_encryption_key1, &keys.locker_decryption_key1) - } else if key_id == keys.locker_key_identifier2 { - (&keys.locker_encryption_key2, &keys.locker_decryption_key2) - } else { - return Err(errors::EncryptionError.into()); - }; - - Ok((public_key.to_string(), private_key.to_string())) -} - -#[cfg(feature = "basilisk")] -#[instrument(skip(state, value1, value2))] -pub async fn old_create_tokenize( - state: &routes::AppState, - value1: String, - value2: Option, - lookup_key: String, -) -> RouterResult { - let payload_to_be_encrypted = api::TokenizePayloadRequest { - value1, - value2: value2.unwrap_or_default(), - lookup_key, - service_name: VAULT_SERVICE_NAME.to_string(), - }; - let payload = utils::Encode::::encode_to_string_of_json( - &payload_to_be_encrypted, - ) - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - let (public_key, private_key) = get_locker_jwe_keys(&state.kms_secrets) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encryption key")?; - let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; - - let create_tokenize_request = api::TokenizePayloadEncrypted { - payload: encrypted_payload, - key_id: get_key_id(&state.conf.jwekey).to_string(), - version: Some(VAULT_VERSION.to_string()), - }; - let request = payment_methods::mk_crud_locker_request( - &state.conf.locker, - "/tokenize", - create_tokenize_request, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Making tokenize request failed")?; - let response = services::call_connector_api(state, request) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - match response { - Ok(r) => { - let resp: api::TokenizePayloadEncrypted = r - .response - .parse_struct("TokenizePayloadEncrypted") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decoding Failed for TokenizePayloadEncrypted")?; - let alg = jwe::RSA_OAEP_256; - let decrypted_payload = services::decrypt_jwe( - &resp.payload, - services::KeyIdCheck::RequestResponseKeyId(( - get_key_id(&state.conf.jwekey), - &resp.key_id, - )), - private_key, - alg, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decrypt Jwe failed for TokenizePayloadEncrypted")?; - let get_response: api::GetTokenizePayloadResponse = decrypted_payload - .parse_struct("GetTokenizePayloadResponse") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "Error getting GetTokenizePayloadResponse from tokenize response", - )?; - Ok(get_response.lookup_key) - } - Err(err) => { - metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); - Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable(format!("Got 4xx from the basilisk locker: {err:?}")) - } - } -} - -#[cfg(feature = "basilisk")] -pub async fn old_get_tokenized_data( - state: &routes::AppState, - lookup_key: &str, - should_get_value2: bool, -) -> RouterResult { - metrics::GET_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); - let payload_to_be_encrypted = api::GetTokenizePayloadRequest { - lookup_key: lookup_key.to_string(), - get_value2: should_get_value2, - service_name: VAULT_SERVICE_NAME.to_string(), - }; - let payload = serde_json::to_string(&payload_to_be_encrypted) - .map_err(|_x| errors::ApiErrorResponse::InternalServerError)?; - - let (public_key, private_key) = get_locker_jwe_keys(&state.kms_secrets) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encryption key")?; - let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; - let create_tokenize_request = api::TokenizePayloadEncrypted { - payload: encrypted_payload, - key_id: get_key_id(&state.conf.jwekey).to_string(), - version: Some("0".to_string()), - }; - let request = payment_methods::mk_crud_locker_request( - &state.conf.locker, - "/tokenize/get", - create_tokenize_request, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Making Get Tokenized request failed")?; - let response = services::call_connector_api(state, request) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; - match response { - Ok(r) => { - let resp: api::TokenizePayloadEncrypted = r - .response - .parse_struct("TokenizePayloadEncrypted") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decoding Failed for TokenizePayloadEncrypted")?; - let alg = jwe::RSA_OAEP_256; - let decrypted_payload = services::decrypt_jwe( - &resp.payload, - services::KeyIdCheck::RequestResponseKeyId(( - get_key_id(&state.conf.jwekey), - &resp.key_id, - )), - private_key, - alg, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("GetTokenizedApi: Decrypt Jwe failed for TokenizePayloadEncrypted")?; - let get_response: api::TokenizePayloadRequest = decrypted_payload - .parse_struct("TokenizePayloadRequest") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting TokenizePayloadRequest from tokenize response")?; - Ok(get_response) - } - Err(err) => { - metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); - match err.status_code { - 404 => Err(errors::ApiErrorResponse::UnprocessableEntity { - message: "Token is invalid or expired".into(), - } - .into()), - _ => Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable(format!("Got error from the basilisk locker: {err:?}")), - } - } - } -} - -#[cfg(feature = "basilisk")] -pub async fn old_delete_tokenized_data( - state: &routes::AppState, - lookup_key: &str, -) -> RouterResult<()> { - metrics::DELETED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); - let payload_to_be_encrypted = api::DeleteTokenizeByTokenRequest { - lookup_key: lookup_key.to_string(), - service_name: VAULT_SERVICE_NAME.to_string(), - }; - let payload = serde_json::to_string(&payload_to_be_encrypted) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error serializing api::DeleteTokenizeByTokenRequest")?; - - let (public_key, _private_key) = get_locker_jwe_keys(&state.kms_secrets.clone()) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encryption key")?; - let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; - let create_tokenize_request = api::TokenizePayloadEncrypted { - payload: encrypted_payload, - key_id: get_key_id(&state.conf.jwekey).to_string(), - version: Some("0".to_string()), - }; - let request = payment_methods::mk_crud_locker_request( - &state.conf.locker, - "/tokenize/delete/token", - create_tokenize_request, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Making Delete Tokenized request failed")?; - let response = services::call_connector_api(state, request) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error while making /tokenize/delete/token call to the locker")?; - match response { - Ok(r) => { - let _delete_response = std::str::from_utf8(&r.response) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decoding Failed for basilisk delete response")?; - Ok(()) - } - Err(err) => { - metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); - Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable(format!("Got 4xx from the basilisk locker: {err:?}")) - } - } -} - -#[cfg(feature = "basilisk")] -pub fn get_key_id(keys: &settings::Jwekey) -> &str { - let key_identifier = "1"; // [#46]: Fetch this value from redis or external sources - if key_identifier == "1" { - &keys.locker_key_identifier1 - } else { - &keys.locker_key_identifier2 - } -} diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 7e19b0b60571..fd265c07da28 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; @@ -12,18 +13,15 @@ pub mod types; use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter}; -use api_models::{ - enums, - payment_methods::{Surcharge, SurchargeDetailsResponse}, - payments::HeaderPayload, -}; -use common_utils::{ext_traits::AsyncExt, pii}; +use api_models::{self, enums, payments::HeaderPayload}; +use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge}; use data_models::mandates::MandateData; use diesel_models::{ephemeral_key, fraud_check::FraudCheck}; 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; @@ -32,17 +30,21 @@ use time; pub use self::operations::{ PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, - PaymentMethodValidate, PaymentReject, PaymentResponse, PaymentSession, PaymentStatus, + PaymentIncrementalAuthorization, 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}; +#[cfg(feature = "frm")] +use crate::core::fraud_check as frm_core; use crate::{ - configs::settings::PaymentMethodTypeTokenFilter, + configs::settings::{ApplePayPreDecryptFlow, PaymentMethodTypeTokenFilter}, core::{ errors::{self, CustomResult, RouterResponse, RouterResult}, payment_methods::PaymentMethodRetrieve, @@ -56,8 +58,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, @@ -66,7 +68,7 @@ use crate::{ workflows::payment_sync, }; -#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments, clippy::type_complexity)] #[instrument(skip_all, fields(payment_id, merchant_id))] pub async fn payments_operation_core( state: &AppState, @@ -76,7 +78,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, @@ -87,7 +89,7 @@ pub async fn payments_operation_core( )> where F: Send + Clone + Sync, - Req: Authenticate, + Req: Authenticate + Clone, Op: Operation + Send + Sync, // To create connector flow specific interface data @@ -112,7 +114,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, @@ -122,6 +129,7 @@ where &merchant_account, &key_store, auth_flow, + header_payload.payment_confirm_source, ) .await?; @@ -137,11 +145,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, @@ -156,154 +167,262 @@ where &mut payment_data, &validate_result, &key_store, + &customer, ) .await?; let mut connector_http_status_code = None; let mut external_latency = None; if let Some(connector_details) = connector { - payment_data = match connector_details { - api::ConnectorCallType::PreDetermined(connector) => { - let schedule_time = if should_add_task_to_process_tracker { - payment_sync::get_sync_process_schedule_time( - &*state.store, - connector.connector.id(), - &merchant_account.merchant_id, - 0, - ) - .await - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting process schedule time")? - } else { - None - }; - let router_data = call_connector_service( - state, - &merchant_account, - &key_store, - connector, - &operation, - &mut payment_data, - &customer, - call_connector_action, - &validate_result, - schedule_time, - header_payload, - ) - .await?; - let operation = Box::new(PaymentResponse); - - connector_http_status_code = router_data.connector_http_status_code; - external_latency = router_data.external_latency; - //add connector http status code metrics - add_connector_http_status_code_metrics(connector_http_status_code); - operation - .to_post_update_tracker()? - .update_tracker( + // Fetch and check FRM configs + #[cfg(feature = "frm")] + let mut frm_info = None; + #[cfg(feature = "frm")] + let db = &*state.store; + #[allow(unused_variables, unused_mut)] + let mut should_continue_transaction: bool = true; + #[cfg(feature = "frm")] + let mut should_continue_capture: bool = true; + #[cfg(feature = "frm")] + let frm_configs = if state.conf.frm.enabled { + frm_core::call_frm_before_connector_call( + db, + &operation, + &merchant_account, + &mut payment_data, + state, + &mut frm_info, + &customer, + &mut should_continue_transaction, + &mut should_continue_capture, + key_store.clone(), + ) + .await? + } else { + None + }; + #[cfg(feature = "frm")] + logger::debug!( + "frm_configs: {:?}\nshould_cancel_transaction: {:?}\nshould_continue_capture: {:?}", + frm_configs, + should_continue_transaction, + should_continue_capture, + ); + + if should_continue_transaction { + #[cfg(feature = "frm")] + match ( + should_continue_capture, + payment_data.payment_attempt.capture_method, + ) { + (false, Some(storage_enums::CaptureMethod::Automatic)) + | (false, Some(storage_enums::CaptureMethod::Scheduled)) => { + payment_data.payment_attempt.capture_method = + Some(storage_enums::CaptureMethod::Manual); + } + _ => (), + }; + payment_data = match connector_details { + api::ConnectorCallType::PreDetermined(connector) => { + let schedule_time = if should_add_task_to_process_tracker { + payment_sync::get_sync_process_schedule_time( + &*state.store, + connector.connector.id(), + &merchant_account.merchant_id, + 0, + ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting process schedule time")? + } else { + None + }; + let router_data = call_connector_service( state, - &validate_result.payment_id, - payment_data, - router_data, - merchant_account.storage_scheme, + &merchant_account, + &key_store, + connector, + &operation, + &mut payment_data, + &customer, + call_connector_action, + &validate_result, + schedule_time, + header_payload, + #[cfg(feature = "frm")] + frm_info.as_ref().and_then(|fi| fi.suggested_action), + #[cfg(not(feature = "frm"))] + None, ) - .await? - } + .await?; + let operation = Box::new(PaymentResponse); + + connector_http_status_code = router_data.connector_http_status_code; + external_latency = router_data.external_latency; + //add connector http status code metrics + add_connector_http_status_code_metrics(connector_http_status_code); + operation + .to_post_update_tracker()? + .update_tracker( + state, + &validate_result.payment_id, + payment_data, + router_data, + merchant_account.storage_scheme, + ) + .await? + } - api::ConnectorCallType::Retryable(connectors) => { - let mut connectors = connectors.into_iter(); + api::ConnectorCallType::Retryable(connectors) => { + let mut connectors = connectors.into_iter(); - let connector_data = get_connector_data(&mut connectors)?; + let connector_data = get_connector_data(&mut connectors)?; - let schedule_time = if should_add_task_to_process_tracker { - payment_sync::get_sync_process_schedule_time( - &*state.store, - connector_data.connector.id(), - &merchant_account.merchant_id, - 0, + let schedule_time = if should_add_task_to_process_tracker { + payment_sync::get_sync_process_schedule_time( + &*state.store, + connector_data.connector.id(), + &merchant_account.merchant_id, + 0, + ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting process schedule time")? + } else { + None + }; + let router_data = call_connector_service( + state, + &merchant_account, + &key_store, + connector_data.clone(), + &operation, + &mut payment_data, + &customer, + call_connector_action, + &validate_result, + schedule_time, + header_payload, + #[cfg(feature = "frm")] + frm_info.as_ref().and_then(|fi| fi.suggested_action), + #[cfg(not(feature = "frm"))] + None, ) - .await - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting process schedule time")? - } else { - None - }; - let router_data = call_connector_service( - state, - &merchant_account, - &key_store, - connector_data.clone(), - &operation, - &mut payment_data, - &customer, - call_connector_action, - &validate_result, - schedule_time, - header_payload, - ) - .await?; + .await?; - #[cfg(feature = "retry")] - let mut router_data = router_data; - #[cfg(feature = "retry")] - { - use crate::core::payments::retry::{self, GsmValidation}; - let config_bool = - retry::config_should_call_gsm(&*state.store, &merchant_account.merchant_id) - .await; + #[cfg(feature = "retry")] + let mut router_data = router_data; + #[cfg(feature = "retry")] + { + use crate::core::payments::retry::{self, GsmValidation}; + let config_bool = retry::config_should_call_gsm( + &*state.store, + &merchant_account.merchant_id, + ) + .await; + + if config_bool && router_data.should_call_gsm() { + router_data = retry::do_gsm_actions( + state, + &mut payment_data, + connectors, + connector_data, + router_data, + &merchant_account, + &key_store, + &operation, + &customer, + &validate_result, + schedule_time, + #[cfg(feature = "frm")] + frm_info.as_ref().and_then(|fi| fi.suggested_action), + #[cfg(not(feature = "frm"))] + None, + ) + .await?; + }; + } - if config_bool && router_data.should_call_gsm() { - router_data = retry::do_gsm_actions( + let operation = Box::new(PaymentResponse); + connector_http_status_code = router_data.connector_http_status_code; + external_latency = router_data.external_latency; + //add connector http status code metrics + add_connector_http_status_code_metrics(connector_http_status_code); + operation + .to_post_update_tracker()? + .update_tracker( state, - &mut payment_data, - connectors, - connector_data, + &validate_result.payment_id, + payment_data, router_data, - &merchant_account, - &key_store, - &operation, - &customer, - &validate_result, - schedule_time, + merchant_account.storage_scheme, ) - .await?; - }; + .await? } - let operation = Box::new(PaymentResponse); - connector_http_status_code = router_data.connector_http_status_code; - external_latency = router_data.external_latency; - //add connector http status code metrics - add_connector_http_status_code_metrics(connector_http_status_code); - operation - .to_post_update_tracker()? - .update_tracker( + api::ConnectorCallType::SessionMultiple(connectors) => { + let session_surcharge_details = + call_surcharge_decision_management_for_session_flow( + state, + &merchant_account, + &mut payment_data, + &connectors, + ) + .await?; + call_multiple_connectors_service( state, - &validate_result.payment_id, + &merchant_account, + &key_store, + connectors, + &operation, payment_data, - router_data, - merchant_account.storage_scheme, + &customer, + session_surcharge_details, ) .await? - } + } + }; - api::ConnectorCallType::SessionMultiple(connectors) => { - let session_surcharge_data = - get_session_surcharge_data(&payment_data.payment_attempt); - call_multiple_connectors_service( + #[cfg(feature = "frm")] + if let Some(fraud_info) = &mut frm_info { + Box::pin(frm_core::post_payment_frm_core( state, &merchant_account, - &key_store, - connectors, - &operation, - payment_data, + &mut payment_data, + fraud_info, + frm_configs + .clone() + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "frm_configs", + }) + .into_report() + .attach_printable("Frm configs label not found")?, &customer, - session_surcharge_data, - ) - .await? + key_store, + )) + .await?; } - }; + } else { + (_, payment_data) = operation + .to_update_tracker()? + .update_trackers( + state, + payment_data.clone(), + customer.clone(), + validate_result.storage_scheme, + None, + &key_store, + #[cfg(feature = "frm")] + frm_info.and_then(|info| info.suggested_action), + #[cfg(not(feature = "frm"))] + None, + header_payload, + ) + .await?; + } + payment_data .payment_attempt .payment_token @@ -334,6 +453,23 @@ where .await?; } + let cloned_payment_data = payment_data.clone(); + let cloned_customer = customer.clone(); + let cloned_request = req.clone(); + + crate::utils::trigger_payments_webhook( + merchant_account, + business_profile, + cloned_payment_data, + Some(cloned_request), + cloned_customer, + state, + operation, + ) + .await + .map_err(|error| logger::warn!(payments_outgoing_webhook_error=?error)) + .ok(); + Ok(( payment_data, req, @@ -343,6 +479,111 @@ 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, +) -> RouterResult<()> +where + F: Send + Clone, +{ + if payment_data + .payment_intent + .surcharge_applicable + .unwrap_or(false) + { + if let Some(surcharge_details) = payment_data.payment_attempt.get_surcharge_details() { + // if retry payment, surcharge would have been populated from the previous attempt. Use the same surcharge + let surcharge_details = + types::SurchargeDetails::from((&surcharge_details, &payment_data.payment_attempt)); + payment_data.surcharge_details = Some(surcharge_details); + return Ok(()); + } + let raw_card_key = payment_data + .payment_method_data + .as_ref() + .and_then(get_key_params_for_surcharge_details) + .map(|(payment_method, payment_method_type, card_network)| { + types::SurchargeKey::PaymentMethodData( + payment_method, + payment_method_type, + card_network, + ) + }); + let saved_card_key = payment_data.token.clone().map(types::SurchargeKey::Token); + + let surcharge_key = raw_card_key + .or(saved_card_key) + .get_required_value("payment_method_data or payment_token")?; + logger::debug!(surcharge_key_confirm =? surcharge_key); + + let calculated_surcharge_details = + match types::SurchargeMetadata::get_individual_surcharge_detail_from_redis( + state, + surcharge_key, + &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)? + } + }; + + payment_data.surcharge_details = calculated_surcharge_details; + } else { + let surcharge_details = + payment_data + .payment_attempt + .get_surcharge_details() + .map(|surcharge_details| { + types::SurchargeDetails::from(( + &surcharge_details, + &payment_data.payment_attempt, + )) + }); + payment_data.surcharge_details = surcharge_details; + } + Ok(()) +} + #[inline] pub fn get_connector_data( connectors: &mut IntoIter, @@ -354,20 +595,60 @@ 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( + types::SurchargeDetails { + original_amount: payment_data.payment_attempt.amount, + 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")?; + + 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( @@ -385,7 +666,7 @@ where F: Send + Clone + Sync, FData: Send + Sync, Op: Operation + Send + Sync + Clone, - Req: Debug + Authenticate, + Req: Debug + Authenticate + Clone, Res: transformers::ToResponse, Op>, // To create connector flow specific interface data PaymentData: ConstructFlowSpecificData, @@ -738,6 +1019,7 @@ pub async fn call_connector_service( validate_result: &operations::ValidateResult<'_>, schedule_time: Option, header_payload: HeaderPayload, + frm_suggestion: Option, ) -> RouterResult> where F: Send + Clone + Sync, @@ -771,6 +1053,11 @@ where merchant_connector_account.get_mca_id(); } + operation + .to_domain()? + .populate_payment_data(state, payment_data, merchant_account) + .await?; + let (pd, tokenization_action) = get_connector_tokenization_action_when_confirm_true( state, operation, @@ -778,6 +1065,7 @@ where validate_result, &merchant_connector_account, key_store, + customer, ) .await?; @@ -908,7 +1196,7 @@ where merchant_account.storage_scheme, updated_customer, key_store, - None, + frm_suggestion, header_payload, ) .await?; @@ -1215,6 +1503,41 @@ where { 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 if connector.connector_name == router_types::Connector::Nmi + && !matches!(format!("{operation:?}").as_str(), "CompleteAuthorize") + && router_data.auth_type == storage_enums::AuthenticationType::ThreeDs + { + router_data = router_data.preprocessing_steps(state, connector).await?; + + (router_data, false) + } else if (connector.connector_name == router_types::Connector::Cybersource + || connector.connector_name == router_types::Connector::Bankofamerica) + && is_operation_complete_authorize(&operation) + && router_data.auth_type == storage_enums::AuthenticationType::ThreeDs + { + router_data = router_data.preprocessing_steps(state, connector).await?; + + // Should continue the flow only if no redirection_data is returned else a response with redirection form shall be returned + let should_continue = matches!( + router_data.response, + Ok(router_types::PaymentsResponseData::TransactionResponse { + redirection_data: None, + .. + }) + ) && router_data.status + != common_enums::AttemptStatus::AuthenticationFailed; + (router_data, should_continue) + } else { + (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) @@ -1232,7 +1555,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) @@ -1286,6 +1623,7 @@ fn is_payment_method_tokenization_enabled_for_connector( connector_name: &str, payment_method: &storage::enums::PaymentMethod, payment_method_type: &Option, + apple_pay_flow: &Option, ) -> RouterResult { let connector_tokenization_filter = state.conf.tokenization.0.get(connector_name); @@ -1299,13 +1637,35 @@ fn is_payment_method_tokenization_enabled_for_connector( payment_method_type, connector_filter.payment_method_type.clone(), ) + && is_apple_pay_pre_decrypt_type_connector_tokenization( + payment_method_type, + apple_pay_flow, + connector_filter.apple_pay_pre_decrypt_flow.clone(), + ) }) .unwrap_or(false)) } +fn is_apple_pay_pre_decrypt_type_connector_tokenization( + payment_method_type: &Option, + apple_pay_flow: &Option, + apple_pay_pre_decrypt_flow_filter: Option, +) -> bool { + match (payment_method_type, apple_pay_flow) { + ( + Some(storage::enums::PaymentMethodType::ApplePay), + Some(enums::ApplePayFlow::Simplified), + ) => !matches!( + apple_pay_pre_decrypt_flow_filter, + Some(ApplePayPreDecryptFlow::NetworkTokenization) + ), + _ => true, + } +} + fn decide_apple_pay_flow( payment_method_type: &Option, - merchant_connector_account: &Option, + merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>, ) -> Option { payment_method_type.and_then(|pmt| match pmt { api_models::enums::PaymentMethodType::ApplePay => { @@ -1316,9 +1676,9 @@ fn decide_apple_pay_flow( } fn check_apple_pay_metadata( - merchant_connector_account: &Option, + merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>, ) -> Option { - merchant_connector_account.clone().and_then(|mca| { + merchant_connector_account.and_then(|mca| { let metadata = mca.get_metadata(); metadata.and_then(|apple_pay_metadata| { let parsed_metadata = apple_pay_metadata @@ -1343,7 +1703,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 { @@ -1459,6 +1819,7 @@ pub async fn get_connector_tokenization_action_when_confirm_true( validate_result: &operations::ValidateResult<'_>, merchant_connector_account: &helpers::MerchantConnectorAccountType, merchant_key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult<(PaymentData, TokenizationAction)> where F: Send + Clone, @@ -1488,19 +1849,18 @@ where .get_required_value("payment_method")?; let payment_method_type = &payment_data.payment_attempt.payment_method_type; + let apple_pay_flow = + decide_apple_pay_flow(payment_method_type, Some(merchant_connector_account)); + let is_connector_tokenization_enabled = is_payment_method_tokenization_enabled_for_connector( state, &connector, payment_method, payment_method_type, + &apple_pay_flow, )?; - let apple_pay_flow = decide_apple_pay_flow( - payment_method_type, - &Some(merchant_connector_account.clone()), - ); - add_apple_pay_flow_metrics( &apple_pay_flow, payment_data.payment_attempt.connector.clone(), @@ -1526,6 +1886,7 @@ where payment_data, validate_result.storage_scheme, merchant_key_store, + customer, ) .await?; payment_data.payment_method_data = payment_method_data; @@ -1541,6 +1902,7 @@ where payment_data, validate_result.storage_scheme, merchant_key_store, + customer, ) .await?; @@ -1578,6 +1940,7 @@ pub async fn tokenize_in_router_when_confirm_false( payment_data: &mut PaymentData, validate_result: &operations::ValidateResult<'_>, merchant_key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult> where F: Send + Clone, @@ -1592,6 +1955,7 @@ where payment_data, validate_result.storage_scheme, merchant_key_store, + customer, ) .await?; payment_data.payment_method_data = payment_method_data; @@ -1657,14 +2021,27 @@ where pub recurring_mandate_payment_data: Option, pub ephemeral_key: Option, pub redirect_response: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub frm_message: Option, pub payment_link_data: Option, + pub incremental_authorization_details: Option, + pub authorizations: Vec, + pub frm_metadata: Option, +} + +#[derive(Debug, Default, Clone)] +pub struct IncrementalAuthorizationDetails { + pub additional_amount: i64, + pub total_amount: i64, + pub reason: Option, + pub authorization_id: Option, } #[derive(Debug, Default, Clone)] pub struct RecurringMandatePaymentData { pub payment_method_type: Option, //required for making recurring payment using saved payment method through stripe + pub original_payment_authorized_amount: Option, + pub original_payment_authorized_currency: Option, } #[derive(Debug, Default, Clone)] @@ -1736,19 +2113,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 @@ -1759,7 +2136,12 @@ pub fn should_call_connector( } "CompleteAuthorize" => true, "PaymentApprove" => true, + "PaymentReject" => true, "PaymentSession" => true, + "PaymentIncrementalAuthorization" => matches!( + payment_data.payment_intent.status, + storage_enums::IntentStatus::RequiresCapture + ), _ => false, } } @@ -1768,6 +2150,10 @@ pub fn is_operation_confirm(operation: &Op) -> bool { matches!(format!("{operation:?}").as_str(), "PaymentConfirm") } +pub fn is_operation_complete_authorize(operation: &Op) -> bool { + matches!(format!("{operation:?}").as_str(), "CompleteAuthorize") +} + #[cfg(feature = "olap")] pub async fn list_payments( state: AppState, @@ -1850,10 +2236,7 @@ pub async fn apply_filters_on_payments( merchant.storage_scheme, ) .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)? - .into_iter() - .map(|(pi, pa)| (pi, pa)) - .collect(); + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; let data: Vec = list.into_iter().map(ForeignFrom::foreign_from).collect(); @@ -1998,11 +2381,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>, @@ -2040,6 +2425,7 @@ where connector_selection( state, merchant_account, + business_profile, key_store, payment_data, Some(straight_through), @@ -2052,6 +2438,7 @@ where connector_selection( state, merchant_account, + business_profile, key_store, payment_data, None, @@ -2075,6 +2462,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, @@ -2114,6 +2502,7 @@ where let decided_connector = decide_connector( state.clone(), merchant_account, + business_profile, key_store, payment_data, request_straight_through, @@ -2141,9 +2530,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, @@ -2345,6 +2736,7 @@ where route_connector_v1( &state, merchant_account, + business_profile, key_store, payment_data, routing_data, @@ -2480,6 +2872,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, @@ -2488,44 +2881,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 46eaca26f7cc..ebc0cf3664af 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -3,6 +3,7 @@ pub mod authorize_flow; pub mod cancel_flow; pub mod capture_flow; pub mod complete_authorize_flow; +pub mod incremental_authorization_flow; pub mod psync_flow; pub mod reject_flow; pub mod session_flow; @@ -10,6 +11,8 @@ pub mod setup_mandate_flow; use async_trait::async_trait; +#[cfg(feature = "frm")] +use crate::types::fraud_check as frm_types; use crate::{ connector, core::{ @@ -144,14 +147,12 @@ impl default_imp_for_complete_authorize!( connector::Aci, connector::Adyen, - connector::Bankofamerica, connector::Bitpay, connector::Boku, connector::Cashtocode, connector::Checkout, connector::Coinbase, connector::Cryptopay, - connector::Cybersource, connector::Dlocal, connector::Fiserv, connector::Forte, @@ -162,13 +163,15 @@ default_imp_for_complete_authorize!( connector::Klarna, connector::Multisafepay, connector::Nexinets, - connector::Nmi, connector::Noon, connector::Opayo, connector::Opennode, connector::Payeezy, connector::Payu, + connector::Placetopay, connector::Rapyd, + connector::Riskified, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -242,10 +245,13 @@ default_imp_for_webhook_source_verification!( connector::Payeezy, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -321,10 +327,13 @@ default_imp_for_create_customer!( connector::Paypal, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, + connector::Signifyd, connector::Square, connector::Trustpay, connector::Tsys, @@ -389,10 +398,13 @@ default_imp_for_connector_redirect_response!( connector::Opennode, connector::Payeezy, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Tsys, @@ -448,10 +460,13 @@ default_imp_for_connector_request_id!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -530,10 +545,13 @@ default_imp_for_accept_dispute!( connector::Paypal, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -630,10 +648,13 @@ default_imp_for_file_upload!( connector::Paypal, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Trustpay, @@ -708,10 +729,13 @@ default_imp_for_submit_evidence!( connector::Paypal, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Trustpay, @@ -786,10 +810,13 @@ default_imp_for_defend_dispute!( connector::Paypal, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -832,11 +859,9 @@ impl default_imp_for_pre_processing_steps!( connector::Aci, - connector::Adyen, connector::Airwallex, connector::Authorizedotnet, connector::Bambora, - connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, @@ -845,7 +870,6 @@ default_imp_for_pre_processing_steps!( connector::Checkout, connector::Coinbase, connector::Cryptopay, - connector::Cybersource, connector::Dlocal, connector::Iatapay, connector::Fiserv, @@ -857,18 +881,19 @@ default_imp_for_pre_processing_steps!( connector::Mollie, connector::Multisafepay, connector::Nexinets, - connector::Nmi, connector::Noon, connector::Nuvei, connector::Opayo, connector::Opennode, connector::Payeezy, - connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Tsys, @@ -926,9 +951,12 @@ default_imp_for_payouts!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1005,9 +1033,12 @@ default_imp_for_payouts_create!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1087,9 +1118,12 @@ default_imp_for_payouts_eligibility!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1166,9 +1200,12 @@ default_imp_for_payouts_fulfill!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1245,9 +1282,12 @@ default_imp_for_payouts_cancel!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1325,9 +1365,12 @@ default_imp_for_payouts_quote!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1405,9 +1448,12 @@ default_imp_for_payouts_recipient!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1484,9 +1530,12 @@ default_imp_for_approve!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1564,9 +1613,653 @@ default_imp_for_reject!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Riskified, + connector::Signifyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_fraud_check { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheck for $path::$connector {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::FraudCheck for connector::DummyConnector {} + +default_imp_for_fraud_check!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Placetopay, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "frm")] +macro_rules! default_imp_for_frm_sale { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckSale for $path::$connector {} + impl + services::ConnectorIntegration< + api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl api::FraudCheckSale for connector::DummyConnector {} +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl + services::ConnectorIntegration< + api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +#[cfg(feature = "frm")] +default_imp_for_frm_sale!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Placetopay, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "frm")] +macro_rules! default_imp_for_frm_checkout { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckCheckout for $path::$connector {} + impl + services::ConnectorIntegration< + api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl api::FraudCheckCheckout for connector::DummyConnector {} +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl + services::ConnectorIntegration< + api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +#[cfg(feature = "frm")] +default_imp_for_frm_checkout!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Placetopay, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "frm")] +macro_rules! default_imp_for_frm_transaction { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckTransaction for $path::$connector {} + impl + services::ConnectorIntegration< + api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl api::FraudCheckTransaction for connector::DummyConnector {} +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl + services::ConnectorIntegration< + api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +#[cfg(feature = "frm")] +default_imp_for_frm_transaction!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Placetopay, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "frm")] +macro_rules! default_imp_for_frm_fulfillment { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckFulfillment for $path::$connector {} + impl + services::ConnectorIntegration< + api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl api::FraudCheckFulfillment for connector::DummyConnector {} +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl + services::ConnectorIntegration< + api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +#[cfg(feature = "frm")] +default_imp_for_frm_fulfillment!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Placetopay, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "frm")] +macro_rules! default_imp_for_frm_record_return { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckRecordReturn for $path::$connector {} + impl + services::ConnectorIntegration< + api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl api::FraudCheckRecordReturn for connector::DummyConnector {} +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl + services::ConnectorIntegration< + api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +#[cfg(feature = "frm")] +default_imp_for_frm_record_return!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Placetopay, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_incremental_authorization { + ($($path:ident::$connector:ident),*) => { + $( + impl api::PaymentIncrementalAuthorization for $path::$connector {} + impl + services::ConnectorIntegration< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::PaymentIncrementalAuthorization for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_incremental_authorization!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Placetopay, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Riskified, + connector::Signifyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_revoking_mandates { + ($($path:ident::$connector:ident),*) => { + $( impl api::ConnectorMandateRevoke for $path::$connector {} + impl + services::ConnectorIntegration< + api::MandateRevoke, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::ConnectorMandateRevoke for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::MandateRevoke, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + > for connector::DummyConnector +{ +} +default_imp_for_revoking_mandates!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 04bd7f0b4338..c6de222f7d83 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -76,12 +76,6 @@ impl Feature for types::PaymentsAu .connector .validate_capture_method(self.request.capture_method) .to_payment_failed_response()?; - if self.request.surcharge_details.is_some() { - connector - .connector - .validate_if_surcharge_implemented() - .to_payment_failed_response()?; - } if self.should_proceed_with_authorize() { self.decide_authentication_type(); @@ -98,7 +92,9 @@ impl Feature for types::PaymentsAu metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics - if resp.request.setup_mandate_details.clone().is_some() { + let is_mandate = resp.request.setup_mandate_details.is_some(); + + if is_mandate { let payment_method_id = Box::pin(tokenization::save_payment_method( state, connector, @@ -380,7 +376,7 @@ impl TryFrom<&types::RouterData for types::PaymentsPreProcessingData complete_authorize_url: data.complete_authorize_url, browser_info: data.browser_info, surcharge_details: data.surcharge_details, + connector_transaction_id: None, + redirect_response: 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: data.complete_authorize_url, + browser_info: data.browser_info, + surcharge_details: None, + connector_transaction_id: data.connector_transaction_id, + redirect_response: data.redirect_response, }) } } 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 44d8728fd4d2..68d0ee8d475f 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, @@ -144,6 +144,85 @@ 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 mut router_data_request = router_data.request.to_owned(); + + if let Ok(types::PaymentsResponseData::TransactionResponse { + connector_metadata, .. + }) = &resp.response + { + router_data_request.connector_meta = connector_metadata.to_owned(); + }; + + let authorize_router_data = + payments::helpers::router_data_type_conversion::<_, F, _, _, _, _>( + resp.clone(), + router_data_request, + 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/incremental_authorization_flow.rs b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs new file mode 100644 index 000000000000..387916bab7c9 --- /dev/null +++ b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs @@ -0,0 +1,118 @@ +use async_trait::async_trait; + +use super::ConstructFlowSpecificData; +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + payments::{self, access_token, helpers, transformers, Feature, PaymentData}, + }, + routes::AppState, + services, + types::{self, api, domain}, +}; + +#[async_trait] +impl + ConstructFlowSpecificData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for PaymentData +{ + async fn construct_router_data<'a>( + &self, + state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult { + Box::pin(transformers::construct_payment_router_data::< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + >( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + )) + .await + } +} + +#[async_trait] +impl Feature + for types::RouterData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > +{ + async fn decide_flows<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + _customer: &Option, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, + connector_request: Option, + _key_store: &domain::MerchantKeyStore, + ) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &self, + call_connector_action, + connector_request, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) + } + + async fn add_access_token<'a>( + &self, + state: &AppState, + connector: &api::ConnectorData, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + access_token::add_access_token(state, connector, merchant_account, self).await + } + + async fn build_flow_specific_connector_request( + &mut self, + state: &AppState, + connector: &api::ConnectorData, + call_connector_action: payments::CallConnectorAction, + ) -> RouterResult<(Option, bool)> { + let request = match call_connector_action { + payments::CallConnectorAction::Trigger => { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + connector_integration + .build_request(self, &state.conf.connectors) + .to_payment_failed_response()? + } + _ => None, + }; + + Ok((request, true)) + } +} diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 595a6f5e958e..099c266e04f2 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -1,9 +1,15 @@ use api_models::payments as payment_types; use async_trait::async_trait; -use common_utils::ext_traits::ByteSliceExt; +use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, Report, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms; +#[cfg(feature = "hashicorp-vault")] +use masking::ExposeInterface; use super::{ConstructFlowSpecificData, Feature}; use crate::{ @@ -15,7 +21,7 @@ use crate::{ routes::{self, metrics}, services, types::{self, api, domain}, - utils::{self, OptionExt}, + utils::OptionExt, }; #[async_trait] @@ -124,13 +130,6 @@ fn build_apple_pay_session_request( apple_pay_merchant_cert: String, apple_pay_merchant_cert_key: String, ) -> RouterResult { - let applepay_session_request = types::RequestBody::log_and_get_request_body( - &request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode ApplePay session request to a string of json")?; - let mut url = state.conf.connectors.applepay.base_url.to_owned(); url.push_str("paymentservices/paymentSession"); @@ -142,7 +141,7 @@ fn build_apple_pay_session_request( headers::CONTENT_TYPE.to_string(), "application/json".to_string().into(), )]) - .body(Some(applepay_session_request)) + .set_body(RequestContent::Json(Box::new(request))) .add_certificate(Some(apple_pay_merchant_cert)) .add_certificate_key(Some(apple_pay_merchant_cert_key)) .build(); @@ -184,10 +183,85 @@ async fn create_applepay_session_token( payment_request_data, session_token_data, } => { + let ( + apple_pay_merchant_cert, + apple_pay_merchant_cert_key, + common_merchant_identifier, + ) = async { + #[cfg(feature = "hashicorp-vault")] + let client = external_services::hashicorp_vault::get_hashicorp_client( + &state.conf.hc_vault, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while building hashicorp client")?; + + #[cfg(feature = "hashicorp-vault")] + { + Ok::<_, Report>(( + masking::Secret::new( + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert + .clone(), + ) + .fetch_inner::(client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)? + .expose(), + masking::Secret::new( + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert_key + .clone(), + ) + .fetch_inner::(client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)? + .expose(), + masking::Secret::new( + state + .conf + .applepay_merchant_configs + .common_merchant_identifier + .clone(), + ) + .fetch_inner::(client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)? + .expose(), + )) + } + + #[cfg(not(feature = "hashicorp-vault"))] + { + Ok::<_, Report>(( + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert + .clone(), + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert_key + .clone(), + state + .conf + .applepay_merchant_configs + .common_merchant_identifier + .clone(), + )) + } + } + .await?; + #[cfg(feature = "kms")] let decrypted_apple_pay_merchant_cert = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_merchant_cert) + .decrypt(apple_pay_merchant_cert) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Apple pay merchant certificate decryption failed")?; @@ -196,7 +270,7 @@ async fn create_applepay_session_token( let decrypted_apple_pay_merchant_cert_key = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_merchant_cert_key) + .decrypt(apple_pay_merchant_cert_key) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( @@ -206,21 +280,13 @@ async fn create_applepay_session_token( #[cfg(feature = "kms")] let decrypted_merchant_identifier = kms::get_kms_client(&state.conf.kms) .await - .decrypt( - &state - .conf - .applepay_merchant_configs - .common_merchant_identifier, - ) + .decrypt(common_merchant_identifier) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Apple pay merchant identifier decryption failed")?; #[cfg(not(feature = "kms"))] - let decrypted_merchant_identifier = &state - .conf - .applepay_merchant_configs - .common_merchant_identifier; + let decrypted_merchant_identifier = common_merchant_identifier; let apple_pay_session_request = get_session_request_for_simplified_apple_pay( decrypted_merchant_identifier.to_string(), @@ -228,12 +294,10 @@ async fn create_applepay_session_token( ); #[cfg(not(feature = "kms"))] - let decrypted_apple_pay_merchant_cert = - &state.conf.applepay_decrypt_keys.apple_pay_merchant_cert; + let decrypted_apple_pay_merchant_cert = apple_pay_merchant_cert; #[cfg(not(feature = "kms"))] - let decrypted_apple_pay_merchant_cert_key = - &state.conf.applepay_decrypt_keys.apple_pay_merchant_cert_key; + let decrypted_apple_pay_merchant_cert_key = apple_pay_merchant_cert_key; ( payment_request_data, @@ -378,13 +442,7 @@ fn get_apple_pay_payment_request( merchant_identifier: &str, ) -> RouterResult { let applepay_payment_request = payment_types::ApplePayPaymentRequest { - country_code: session_data - .country - .to_owned() - .get_required_value("country_code") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "country_code", - })?, + country_code: session_data.country, currency_code: session_data.currency, total: amount_info, merchant_capabilities: Some(payment_request_data.merchant_capabilities), 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 0c03c8ce123b..8b0b54158fd0 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -1,9 +1,10 @@ use async_trait::async_trait; +use error_stack::{IntoReport, ResultExt}; use super::{ConstructFlowSpecificData, Feature}; use crate::{ core::{ - errors::{self, ConnectorErrorExt, RouterResult}, + errors::{self, ConnectorErrorExt, RouterResult, StorageErrorExt}, mandate, payments::{ self, access_token, customers, helpers, tokenization, transformers, PaymentData, @@ -65,16 +66,16 @@ impl Feature for types::Setup types::SetupMandateRequestData, types::PaymentsResponseData, > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( state, connector_integration, &self, - call_connector_action, + call_connector_action.clone(), connector_request, ) .await .to_setup_mandate_failed_response()?; - let pm_id = Box::pin(tokenization::save_payment_method( state, connector, @@ -86,14 +87,84 @@ impl Feature for types::Setup )) .await?; - mandate::mandate_procedure( - state, - resp, - maybe_customer, - pm_id, - connector.merchant_connector_id.clone(), - ) - .await + if let Some(mandate_id) = self + .request + .setup_mandate_details + .as_ref() + .and_then(|mandate_data| mandate_data.update_mandate_id.clone()) + { + let mandate = state + .store + .find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, &mandate_id) + .await + .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; + + let profile_id = mandate::helpers::get_profile_id_for_mandate( + state, + merchant_account, + mandate.clone(), + ) + .await?; + match resp.response { + Ok(types::PaymentsResponseData::TransactionResponse { .. }) => { + let connector_integration: services::BoxedConnectorIntegration< + '_, + types::api::MandateRevoke, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + > = connector.connector.get_connector_integration(); + let merchant_connector_account = helpers::get_merchant_connector_account( + state, + &merchant_account.merchant_id, + None, + key_store, + &profile_id, + &mandate.connector, + mandate.merchant_connector_id.as_ref(), + ) + .await?; + + let router_data = mandate::utils::construct_mandate_revoke_router_data( + merchant_connector_account, + merchant_account, + mandate.clone(), + ) + .await?; + + let _response = services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + call_connector_action, + None, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + // TODO:Add the revoke mandate task to process tracker + mandate::update_mandate_procedure( + state, + resp, + mandate, + &merchant_account.merchant_id, + pm_id, + ) + .await + } + Ok(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Unexpected response received")?, + Err(_) => Ok(resp), + } + } else { + mandate::mandate_procedure( + state, + resp, + maybe_customer, + pm_id, + connector.merchant_connector_id.clone(), + ) + .await + } } async fn add_access_token<'a>( @@ -208,6 +279,7 @@ impl types::SetupMandateRouterData { .to_setup_mandate_failed_response()?; let payment_method_type = self.request.payment_method_type; + let pm_id = Box::pin(tokenization::save_payment_method( state, connector, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index b9e96ec36e11..fe4a7757ecc9 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, RequestSurchargeDetails}; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, ValueExt}, @@ -12,6 +13,10 @@ use data_models::{ use diesel_models::enums; // TODO : Evaluate all the helper functions () use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms; use josekit::jwe; @@ -22,7 +27,6 @@ use openssl::{ symm::{decrypt_aead, Cipher}, }; use router_env::{instrument, logger, tracing}; -use time::Duration; use uuid::Uuid; use x509_parser::parse_x509_certificate; @@ -55,7 +59,7 @@ use crate::{ utils::{ self, crypto::{self, SignMessage}, - OptionExt, + OptionExt, StringExt, }, }; @@ -475,6 +479,27 @@ pub async fn get_token_for_recurring_mandate( .await .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; + let original_payment_intent = mandate + .original_payment_id + .as_ref() + .async_map(|payment_id| async { + db.find_payment_intent_by_payment_id_merchant_id( + payment_id, + &mandate.merchant_id, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .map_err(|err| logger::error!(mandate_original_payment_not_found=?err)) + .ok() + }) + .await + .flatten(); + + let original_payment_authorized_amount = original_payment_intent.clone().map(|pi| pi.amount); + let original_payment_authorized_currency = + original_payment_intent.clone().and_then(|pi| pi.currency); + let customer = req.customer_id.clone().get_required_value("customer_id")?; let payment_method_id = { @@ -509,16 +534,23 @@ pub async fn get_token_for_recurring_mandate( }; if let diesel_models::enums::PaymentMethod::Card = payment_method.payment_method { - let _ = - cards::get_lookup_key_from_locker(state, &token, &payment_method, merchant_key_store) - .await?; + if state.conf.locker.locker_enabled { + let _ = cards::get_lookup_key_from_locker( + state, + &token, + &payment_method, + merchant_key_store, + ) + .await?; + } + if let Some(payment_method_from_request) = req.payment_method { let pm: storage_enums::PaymentMethod = payment_method_from_request; if pm != payment_method.payment_method { Err(report!(errors::ApiErrorResponse::PreconditionFailed { message: "payment method in request does not match previously provided payment \ - method information" + method information" .into() }))? } @@ -529,6 +561,8 @@ pub async fn get_token_for_recurring_mandate( Some(payment_method.payment_method), Some(payments::RecurringMandatePaymentData { payment_method_type, + original_payment_authorized_amount, + original_payment_authorized_currency, }), payment_method.payment_method_type, Some(mandate_connector_details), @@ -539,6 +573,8 @@ pub async fn get_token_for_recurring_mandate( Some(payment_method.payment_method), Some(payments::RecurringMandatePaymentData { payment_method_type, + original_payment_authorized_amount, + original_payment_authorized_currency, }), payment_method.payment_method_type, Some(mandate_connector_details), @@ -571,6 +607,7 @@ pub fn validate_merchant_id( pub fn validate_request_amount_and_amount_to_capture( op_amount: Option, op_amount_to_capture: Option, + surcharge_details: Option, ) -> CustomResult<(), errors::ApiErrorResponse> { match (op_amount, op_amount_to_capture) { (None, _) => Ok(()), @@ -580,7 +617,11 @@ pub fn validate_request_amount_and_amount_to_capture( api::Amount::Value(amount_inner) => { // If both amount and amount to capture is present // then amount to be capture should be less than or equal to request amount - utils::when(!amount_to_capture.le(&amount_inner.get()), || { + let total_capturable_amount = amount_inner.get() + + surcharge_details + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + .unwrap_or(0); + utils::when(!amount_to_capture.le(&total_capturable_amount), || { Err(report!(errors::ApiErrorResponse::PreconditionFailed { message: format!( "amount_to_capture is greater than amount capture_amount: {amount_to_capture:?} request_amount: {amount:?}" @@ -600,6 +641,50 @@ 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_and_capture_method( + payment_attempt: Option<&PaymentAttempt>, + request: &api_models::payments::PaymentsRequest, +) -> CustomResult<(), errors::ApiErrorResponse> { + let capture_method = request + .capture_method + .or(payment_attempt + .map(|payment_attempt| payment_attempt.capture_method.unwrap_or_default())) + .unwrap_or_default(); + if capture_method == api_enums::CaptureMethod::Automatic { + let original_amount = request + .amount + .map(|amount| amount.into()) + .or(payment_attempt.map(|payment_attempt| payment_attempt.amount)); + let surcharge_amount = request + .surcharge_details + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + .or_else(|| { + payment_attempt.map(|payment_attempt| { + payment_attempt.surcharge_amount.unwrap_or(0) + + payment_attempt.tax_amount.unwrap_or(0) + }) + }) + .unwrap_or(0); + let total_capturable_amount = + original_amount.map(|original_amount| original_amount + surcharge_amount); + if let Some((total_capturable_amount, amount_to_capture)) = + total_capturable_amount.zip(request.amount_to_capture) + { + 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, @@ -755,7 +840,7 @@ fn validate_new_mandate_request( let mandate_details = match mandate_data.mandate_type { Some(api_models::payments::MandateType::SingleUse(details)) => Some(details), Some(api_models::payments::MandateType::MultiUse(details)) => details, - None => None, + _ => None, }; mandate_details.and_then(|md| md.start_date.zip(md.end_date)).map(|(start_date, end_date)| utils::when (start_date >= end_date, || { @@ -903,7 +988,7 @@ pub fn payment_attempt_status_fsm( ) -> storage_enums::AttemptStatus { match payment_method_data { Some(_) => match confirm { - Some(true) => storage_enums::AttemptStatus::Pending, + Some(true) => storage_enums::AttemptStatus::PaymentMethodAwaited, _ => storage_enums::AttemptStatus::ConfirmationAwaited, }, None => storage_enums::AttemptStatus::PaymentMethodAwaited, @@ -916,13 +1001,12 @@ pub fn payment_intent_status_fsm( ) -> storage_enums::IntentStatus { match payment_method_data { Some(_) => match confirm { - Some(true) => storage_enums::IntentStatus::RequiresCustomerAction, + Some(true) => storage_enums::IntentStatus::RequiresPaymentMethod, _ => storage_enums::IntentStatus::RequiresConfirmation, }, None => storage_enums::IntentStatus::RequiresPaymentMethod, } } - pub async fn add_domain_task_to_pt( operation: &Op, state: &AppState, @@ -937,14 +1021,30 @@ where match schedule_time { Some(stime) => { if !requeue { - // scheduler_metrics::TASKS_ADDED_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics + // Here, increment the count of added tasks every time a payment has been confirmed or PSync has been called + metrics::TASKS_ADDED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes( + "flow", + format!("{:#?}", operation), + )], + ); super::add_process_sync_task(&*state.store, payment_attempt, stime) .await .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while adding task to process tracker") } else { - // scheduler_metrics::TASKS_RESET_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics + // When the requeue is true, we reset the tasks count as we reset the task every time it is requeued + metrics::TASKS_RESET_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes( + "flow", + format!("{:#?}", operation), + )], + ); super::reset_process_sync_task(&*state.store, payment_attempt, stime) .await .into_report() @@ -983,8 +1083,12 @@ pub(crate) async fn get_payment_method_create_request( card_number: card.card_number.clone(), card_exp_month: card.card_exp_month.clone(), card_exp_year: card.card_exp_year.clone(), - card_holder_name: Some(card.card_holder_name.clone()), + card_holder_name: card.card_holder_name.clone(), nick_name: card.nick_name.clone(), + card_issuing_country: card.card_issuing_country.clone(), + card_network: card.card_network.clone(), + card_issuer: card.card_issuer.clone(), + card_type: card.card_type.clone(), }; let customer_id = customer.customer_id.clone(); let payment_method_request = api::PaymentMethodCreate { @@ -992,6 +1096,7 @@ pub(crate) async fn get_payment_method_create_request( payment_method_type, payment_method_issuer: card.card_issuer.clone(), payment_method_issuer_code: None, + bank_transfer: None, card: Some(card_detail), metadata: None, customer_id: Some(customer_id), @@ -1008,6 +1113,7 @@ pub(crate) async fn get_payment_method_create_request( payment_method_type, payment_method_issuer: None, payment_method_issuer_code: None, + bank_transfer: None, card: None, metadata: None, customer_id: Some(customer.customer_id.to_owned()), @@ -1173,6 +1279,7 @@ pub async fn get_connector_default( } #[instrument(skip_all)] +#[allow(clippy::type_complexity)] pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>( operation: BoxedOperation<'a, F, R, Ctx>, db: &dyn StorageInterface, @@ -1326,20 +1433,189 @@ 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, + 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; + + // The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked + // from payment_method_data.card_token object + let name_on_card = if let Some(name) = card.card_holder_name.clone() { + if name.clone().expose().is_empty() { + card_token_data + .and_then(|token_data| { + is_card_updated = true; + token_data.card_holder_name.clone() + }) + .or(Some(name)) + } else { + card.card_holder_name.clone() + } + } else { + card_token_data.and_then(|token_data| { + is_card_updated = true; + token_data.card_holder_name.clone() + }) + }; + + updated_card.card_holder_name = name_on_card; + + if let Some(token_data) = card_token_data { + if let Some(cvc) = token_data.card_cvc.clone() { + 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_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")?; + + // The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked + // from payment_method_data.card_token object + let name_on_card = if let Some(name) = card.name_on_card.clone() { + if name.clone().expose().is_empty() { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .or(Some(name)) + } else { + card.name_on_card + } + } else { + card_token_data.and_then(|token_data| token_data.card_holder_name.clone()) + }; + + let api_card = api::Card { + card_number: card.card_number, + card_holder_name: name_on_card, + card_exp_month: card.card_exp_month, + card_exp_year: card.card_exp_year, + card_cvc: card_token_data + .cloned() + .unwrap_or_default() + .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, payment_data: &mut PaymentData, merchant_key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, R, Ctx>, Option, )> { let request = &payment_data.payment_method_data.clone(); + + let mut card_token_data = payment_data + .payment_method_data + .clone() + .and_then(|pmd| match pmd { + api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), + _ => None, + }) + .or(Some(CardToken::default())); + + if let Some(cvc) = payment_data.card_cvc.clone() { + if let Some(token_data) = card_token_data.as_mut() { + token_data.card_cvc = Some(cvc); + } + } + 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 +1634,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,84 +1645,54 @@ 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 } } }; - let card_cvc = payment_data.card_cvc.clone(); - // 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_token_data.as_ref(), + customer, ) .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 +1741,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 +1829,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() })) }, ) @@ -2185,24 +2436,23 @@ pub fn generate_mandate( pub fn authenticate_client_secret( request_client_secret: Option<&String>, payment_intent: &PaymentIntent, - merchant_intent_fulfillment_time: Option, ) -> Result<(), errors::ApiErrorResponse> { match (request_client_secret, &payment_intent.client_secret) { (Some(req_cs), Some(pi_cs)) => { if req_cs != pi_cs { Err(errors::ApiErrorResponse::ClientSecretInvalid) } else { - //This is done to check whether the merchant_account's intent fulfillment time has expired or not - let payment_intent_fulfillment_deadline = - payment_intent.created_at.saturating_add(Duration::seconds( - merchant_intent_fulfillment_time - .unwrap_or(consts::DEFAULT_FULFILLMENT_TIME), - )); let current_timestamp = common_utils::date_time::now(); - fp_utils::when( - current_timestamp > payment_intent_fulfillment_deadline, - || Err(errors::ApiErrorResponse::ClientSecretExpired), - ) + + let session_expiry = payment_intent.session_expiry.unwrap_or( + payment_intent + .created_at + .saturating_add(time::Duration::seconds(consts::DEFAULT_SESSION_EXPIRY)), + ); + + fp_utils::when(current_timestamp > session_expiry, || { + Err(errors::ApiErrorResponse::ClientSecretExpired) + }) } } // If there is no client in payment intent, then it has expired @@ -2211,24 +2461,18 @@ pub fn authenticate_client_secret( } } -pub async fn get_merchant_fullfillment_time( - payment_link_id: Option, - intent_fulfillment_time: Option, - db: &dyn StorageInterface, -) -> RouterResult> { - if let Some(payment_link_id) = payment_link_id { - let payment_link_db = db - .find_payment_link_by_payment_link_id(&payment_link_id) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; - - let curr_time = common_utils::date_time::now(); - Ok(payment_link_db - .fulfilment_time - .map(|merchant_expiry_time| (merchant_expiry_time - curr_time).whole_seconds())) - } else { - Ok(intent_fulfillment_time) - } +pub(crate) fn validate_payment_status_against_allowed_statuses( + intent_status: &storage_enums::IntentStatus, + allowed_statuses: &[storage_enums::IntentStatus], + action: &'static str, +) -> Result<(), errors::ApiErrorResponse> { + fp_utils::when(!allowed_statuses.contains(intent_status), || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "You cannot {action} this payment because it has status {intent_status}", + ), + }) + }) } pub(crate) fn validate_payment_status_against_not_allowed_statuses( @@ -2289,14 +2533,7 @@ pub async fn verify_payment_intent_time_and_client_secret( .await .change_context(errors::ApiErrorResponse::PaymentNotFound)?; - let intent_fulfillment_time = get_merchant_fullfillment_time( - payment_intent.payment_link_id.clone(), - merchant_account.intent_fulfillment_time, - db, - ) - .await?; - - authenticate_client_secret(Some(&cs), &payment_intent, intent_fulfillment_time)?; + authenticate_client_secret(Some(&cs), &payment_intent)?; Ok(payment_intent) }) .await @@ -2407,6 +2644,7 @@ mod tests { modified_at: common_utils::date_time::now(), last_synced: None, setup_future_usage: None, + fingerprint_id: None, off_session: None, client_secret: Some("1".to_string()), active_attempt: data_models::RemoteStorageObject::ForeignID("nopes".to_string()), @@ -2423,15 +2661,19 @@ mod tests { payment_confirm_source: None, surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), + request_incremental_authorization: Some( + common_enums::RequestIncrementalAuthorization::default(), + ), + incremental_authorization_allowed: None, + authorization_count: None, + session_expiry: Some( + common_utils::date_time::now() + .saturating_add(time::Duration::seconds(consts::DEFAULT_SESSION_EXPIRY)), + ), }; let req_cs = Some("1".to_string()); - let merchant_fulfillment_time = Some(900); - assert!(authenticate_client_secret( - req_cs.as_ref(), - &payment_intent, - merchant_fulfillment_time, - ) - .is_ok()); // Check if the result is an Ok variant + assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent).is_ok()); + // Check if the result is an Ok variant } #[test] @@ -2453,8 +2695,9 @@ mod tests { billing_address_id: None, statement_descriptor_name: None, statement_descriptor_suffix: None, - created_at: common_utils::date_time::now().saturating_sub(Duration::seconds(20)), + created_at: common_utils::date_time::now().saturating_sub(time::Duration::seconds(20)), modified_at: common_utils::date_time::now(), + fingerprint_id: None, last_synced: None, setup_future_usage: None, off_session: None, @@ -2473,15 +2716,18 @@ mod tests { payment_confirm_source: None, surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), + request_incremental_authorization: Some( + common_enums::RequestIncrementalAuthorization::default(), + ), + incremental_authorization_allowed: None, + authorization_count: None, + session_expiry: Some( + common_utils::date_time::now() + .saturating_add(time::Duration::seconds(consts::DEFAULT_SESSION_EXPIRY)), + ), }; let req_cs = Some("1".to_string()); - let merchant_fulfillment_time = Some(10); - assert!(authenticate_client_secret( - req_cs.as_ref(), - &payment_intent, - merchant_fulfillment_time, - ) - .is_err()) + assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent,).is_err()) } #[test] @@ -2503,12 +2749,13 @@ mod tests { billing_address_id: None, statement_descriptor_name: None, statement_descriptor_suffix: None, - created_at: common_utils::date_time::now().saturating_sub(Duration::seconds(20)), + created_at: common_utils::date_time::now().saturating_sub(time::Duration::seconds(20)), modified_at: common_utils::date_time::now(), last_synced: None, setup_future_usage: None, off_session: None, client_secret: None, + fingerprint_id: None, active_attempt: data_models::RemoteStorageObject::ForeignID("nopes".to_string()), business_country: None, business_label: None, @@ -2523,15 +2770,18 @@ mod tests { payment_confirm_source: None, surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), + request_incremental_authorization: Some( + common_enums::RequestIncrementalAuthorization::default(), + ), + incremental_authorization_allowed: None, + authorization_count: None, + session_expiry: Some( + common_utils::date_time::now() + .saturating_add(time::Duration::seconds(consts::DEFAULT_SESSION_EXPIRY)), + ), }; let req_cs = Some("1".to_string()); - let merchant_fulfillment_time = Some(10); - assert!(authenticate_client_secret( - req_cs.as_ref(), - &payment_intent, - merchant_fulfillment_time, - ) - .is_err()) + assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent).is_err()) } } @@ -2746,6 +2996,9 @@ pub fn router_data_type_conversion( connector_http_status_code: router_data.connector_http_status_code, external_latency: router_data.external_latency, apple_pay_flow: router_data.apple_pay_flow, + frm_metadata: router_data.frm_metadata, + refund_id: router_data.refund_id, + dispute_id: router_data.dispute_id, } } @@ -2784,6 +3037,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 @@ -2844,6 +3098,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 { @@ -2899,8 +3154,8 @@ impl AttemptType { error_message: None, offer_amount: old_payment_attempt.offer_amount, - surcharge_amount: old_payment_attempt.surcharge_amount, - tax_amount: old_payment_attempt.tax_amount, + surcharge_amount: None, + tax_amount: None, payment_method_id: None, payment_method: None, capture_method: old_payment_attempt.capture_method, @@ -2941,6 +3196,9 @@ impl AttemptType { authentication_data: None, encoded_data: None, merchant_connector_id: None, + unified_code: None, + unified_message: None, + net_amount: old_payment_attempt.amount, } } @@ -3023,6 +3281,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 @@ -3042,6 +3301,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), @@ -3090,6 +3350,7 @@ pub async fn get_additional_payment_data( match pm_data { api_models::payments::PaymentMethodData::Card(card_data) => { let card_isin = Some(card_data.card_number.clone().get_card_isin()); + let card_extended_bin = Some(card_data.card_number.clone().get_card_extended_bin()); let last4 = Some(card_data.card_number.clone().get_last4()); if card_data.card_issuer.is_some() && card_data.card_network.is_some() @@ -3106,9 +3367,10 @@ pub async fn get_additional_payment_data( bank_code: card_data.bank_code.to_owned(), card_exp_month: Some(card_data.card_exp_month.clone()), card_exp_year: Some(card_data.card_exp_year.clone()), - card_holder_name: Some(card_data.card_holder_name.clone()), + card_holder_name: card_data.card_holder_name.clone(), last4: last4.clone(), card_isin: card_isin.clone(), + card_extended_bin: card_extended_bin.clone(), }, )) } else { @@ -3132,26 +3394,30 @@ pub async fn get_additional_payment_data( card_issuing_country: card_info.card_issuing_country, last4: last4.clone(), card_isin: card_isin.clone(), + card_extended_bin: card_extended_bin.clone(), card_exp_month: Some(card_data.card_exp_month.clone()), card_exp_year: Some(card_data.card_exp_year.clone()), - card_holder_name: Some(card_data.card_holder_name.clone()), + card_holder_name: card_data.card_holder_name.clone(), }, )) }); - card_info.unwrap_or(api_models::payments::AdditionalPaymentData::Card(Box::new( - api_models::payments::AdditionalCardInfo { - card_issuer: None, - card_network: None, - bank_code: None, - card_type: None, - card_issuing_country: None, - last4, - card_isin, - card_exp_month: Some(card_data.card_exp_month.clone()), - card_exp_year: Some(card_data.card_exp_year.clone()), - card_holder_name: Some(card_data.card_holder_name.clone()), - }, - ))) + card_info.unwrap_or_else(|| { + api_models::payments::AdditionalPaymentData::Card(Box::new( + api_models::payments::AdditionalCardInfo { + card_issuer: None, + card_network: None, + bank_code: None, + card_type: None, + card_issuing_country: None, + last4, + card_isin, + card_extended_bin, + card_exp_month: Some(card_data.card_exp_month.clone()), + card_exp_year: Some(card_data.card_exp_year.clone()), + card_holder_name: card_data.card_holder_name.clone(), + }, + )) + }) } } api_models::payments::PaymentMethodData::BankRedirect(bank_redirect_data) => { @@ -3202,6 +3468,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 {} + } } } @@ -3264,15 +3533,38 @@ impl ApplePayData { &self, state: &AppState, ) -> CustomResult { + let apple_pay_ppc = async { + #[cfg(feature = "hashicorp-vault")] + let client = + external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")?; + + #[cfg(feature = "hashicorp-vault")] + let output = + masking::Secret::new(state.conf.applepay_decrypt_keys.apple_pay_ppc.clone()) + .fetch_inner::(client) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed)? + .expose(); + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.applepay_decrypt_keys.apple_pay_ppc.clone(); + + Ok::<_, error_stack::Report>(output) + } + .await?; + #[cfg(feature = "kms")] let cert_data = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_ppc) + .decrypt(&apple_pay_ppc) .await .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; #[cfg(not(feature = "kms"))] - let cert_data = &state.conf.applepay_decrypt_keys.apple_pay_ppc; + let cert_data = &apple_pay_ppc; let base64_decode_cert_data = BASE64_ENGINE .decode(cert_data) @@ -3324,15 +3616,39 @@ impl ApplePayData { .change_context(errors::ApplePayDecryptionError::KeyDeserializationFailed) .attach_printable("Failed to deserialize the public key")?; + let apple_pay_ppc_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = + external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")?; + + #[cfg(feature = "hashicorp-vault")] + let output = + masking::Secret::new(state.conf.applepay_decrypt_keys.apple_pay_ppc_key.clone()) + .fetch_inner::(client) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")? + .expose(); + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.applepay_decrypt_keys.apple_pay_ppc_key.clone(); + + Ok::<_, error_stack::Report>(output) + } + .await?; + #[cfg(feature = "kms")] let decrypted_apple_pay_ppc_key = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_ppc_key) + .decrypt(&apple_pay_ppc_key) .await .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; #[cfg(not(feature = "kms"))] - let decrypted_apple_pay_ppc_key = &state.conf.applepay_decrypt_keys.apple_pay_ppc_key; + let decrypted_apple_pay_ppc_key = &apple_pay_ppc_key; // Create PKey objects from EcKey let private_key = PKey::private_key_from_pem(decrypted_apple_pay_ppc_key.as_bytes()) .into_report() @@ -3390,8 +3706,12 @@ impl ApplePayData { .into_report() .change_context(errors::ApplePayDecryptionError::Base64DecodingFailed)?; let iv = [0u8; 16]; //Initialization vector IV is typically used in AES-GCM (Galois/Counter Mode) encryption for randomizing the encryption process. - let ciphertext = &data[..data.len() - 16]; - let tag = &data[data.len() - 16..]; + let ciphertext = data + .get(..data.len() - 16) + .ok_or(errors::ApplePayDecryptionError::DecryptionFailed)?; + let tag = data + .get(data.len() - 16..) + .ok_or(errors::ApplePayDecryptionError::DecryptionFailed)?; let cipher = Cipher::aes_256_gcm(); let decrypted_data = decrypt_aead(cipher, symmetric_key, Some(&iv), &[], ciphertext, tag) .into_report() @@ -3404,23 +3724,85 @@ impl ApplePayData { } } +pub fn get_key_params_for_surcharge_details( + payment_method_data: &api_models::payments::PaymentMethodData, +) -> Option<( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, +)> { + match payment_method_data { + api_models::payments::PaymentMethodData::Card(card) => { + // surcharge generated will always be same for credit as well as debit + // since surcharge conditions cannot be defined on card_type + Some(( + common_enums::PaymentMethod::Card, + common_enums::PaymentMethodType::Credit, + card.card_network.clone(), + )) + } + api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Some(( + common_enums::PaymentMethod::CardRedirect, + card_redirect_data.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::Wallet(wallet) => Some(( + common_enums::PaymentMethod::Wallet, + wallet.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::PayLater(pay_later) => Some(( + common_enums::PaymentMethod::PayLater, + pay_later.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankRedirect(bank_redirect) => Some(( + common_enums::PaymentMethod::BankRedirect, + bank_redirect.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankDebit(bank_debit) => Some(( + common_enums::PaymentMethod::BankDebit, + bank_debit.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankTransfer(bank_transfer) => Some(( + common_enums::PaymentMethod::BankTransfer, + bank_transfer.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::Crypto(crypto) => Some(( + common_enums::PaymentMethod::Crypto, + crypto.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::MandatePayment => None, + api_models::payments::PaymentMethodData::Reward => None, + api_models::payments::PaymentMethodData::Upi(_) => Some(( + common_enums::PaymentMethod::Upi, + common_enums::PaymentMethodType::UpiCollect, + None, + )), + api_models::payments::PaymentMethodData::Voucher(voucher) => Some(( + common_enums::PaymentMethod::Voucher, + voucher.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::GiftCard(gift_card) => Some(( + common_enums::PaymentMethod::GiftCard, + gift_card.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::CardToken(_) => None, + } +} + pub fn validate_payment_link_request( - payment_link_object: &api_models::payments::PaymentLinkObject, confirm: Option, - order_details: Option>, ) -> Result<(), errors::ApiErrorResponse> { if let Some(cnf) = confirm { if !cnf { - let current_time = Some(common_utils::date_time::now()); - if current_time > payment_link_object.link_expiry { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "link_expiry time cannot be less than current time".to_string(), - }); - } else if order_details.is_none() { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "cannot create payment link without order details".to_string(), - }); - } + return Ok(()); } else { return Err(errors::ApiErrorResponse::InvalidRequestData { message: "cannot confirm a payment while creating a payment link".to_string(), @@ -3429,3 +3811,81 @@ 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, + should_validate: bool, +) -> Result<(), errors::ApiErrorResponse> { + if should_validate { + 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(()) + } + } else { + Ok(()) + } +} + +// This function validates the client secret expiry set by the merchant in the request +pub fn validate_session_expiry(session_expiry: u32) -> Result<(), errors::ApiErrorResponse> { + if !(consts::MIN_SESSION_EXPIRY..=consts::MAX_SESSION_EXPIRY).contains(&session_expiry) { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "session_expiry should be between 60(1 min) to 7890000(3 months).".to_string(), + }) + } else { + Ok(()) + } +} diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index f65e65459e00..89d3131ddef1 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -4,13 +4,13 @@ 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; pub mod payment_start; pub mod payment_status; pub mod payment_update; +pub mod payments_incremental_authorization; use api_models::enums::FrmSuggestion; use async_trait::async_trait; @@ -20,10 +20,10 @@ 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, + payments_incremental_authorization::PaymentIncrementalAuthorization, }; use super::{helpers, CustomerDetails, PaymentData}; use crate::{ @@ -91,8 +91,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 +110,8 @@ 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)>; + payment_confirm_source: Option, + ) -> RouterResult>; } #[async_trait] @@ -124,6 +132,7 @@ pub trait Domain: Send + Sync { payment_data: &mut PaymentData, storage_scheme: enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, R, Ctx>, Option, @@ -147,6 +156,15 @@ 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, + _merchant_account: &domain::MerchantAccount, + ) -> CustomResult<(), errors::ApiErrorResponse> { + Ok(()) + } } #[async_trait] @@ -235,11 +253,19 @@ where payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await + helpers::make_pm_data( + Box::new(self), + state, + payment_data, + merchant_key_store, + customer, + ) + .await } } @@ -285,6 +311,7 @@ where _payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, _merchant_key_store: &domain::MerchantKeyStore, + _customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsCaptureRequest, Ctx>, Option, @@ -347,6 +374,7 @@ where _payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, _merchant_key_store: &domain::MerchantKeyStore, + _customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsCancelRequest, Ctx>, Option, @@ -399,6 +427,7 @@ where _payment_data: &mut PaymentData, _storage_scheme: enums::MerchantStorageScheme, _merchant_key_store: &domain::MerchantKeyStore, + _customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRejectRequest, Ctx>, Option, diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 538e65e4b22e..ce3998506c9e 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -2,58 +2,52 @@ 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::{IntoReport, ResultExt}; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ core::{ - errors::{self, CustomResult, RouterResult, StorageErrorExt}, + errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, utils as core_utils, }, - db::StorageInterface, routes::AppState, services, types::{ - self, api::{self, PaymentIdTypeExt}, domain, storage::{self, enums as storage_enums}, }, - utils::{self, OptionExt}, + utils::OptionExt, }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "capture")] pub struct PaymentApprove; #[async_trait] impl - GetTracker, api::PaymentsRequest, Ctx> for PaymentApprove + GetTracker, api::PaymentsCaptureRequest, Ctx> for PaymentApprove { #[instrument(skip_all)] async fn get_trackers<'a>( &'a self, state: &'a AppState, payment_id: &api::PaymentIdType, - request: &api::PaymentsRequest, - mandate_type: Option, + _request: &api::PaymentsCaptureRequest, + _mandate_type: Option, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + _payment_confirm_source: Option, + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; - let (mut payment_intent, mut payment_attempt, currency, amount); + let (mut payment_intent, payment_attempt, currency, amount); let payment_id = payment_id .get_payment_intent_id() @@ -63,9 +57,6 @@ impl .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - payment_intent.setup_future_usage = request - .setup_future_usage - .or(payment_intent.setup_future_usage); helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, @@ -73,32 +64,22 @@ impl storage_enums::IntentStatus::Failed, storage_enums::IntentStatus::Succeeded, ], - "confirm", + "approve", )?; - let ( - token, - payment_method, - payment_method_type, - setup_mandate, - recurring_mandate_payment_data, - mandate_connector, - ) = helpers::get_token_pm_type_mandate_details( - state, - request, - mandate_type.clone(), - merchant_account, - key_store, - ) - .await?; + 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 browser_info = request - .browser_info - .clone() - .map(|x| utils::Encode::::encode_to_value(&x)) - .transpose() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "browser_info", + 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 attempt_id = payment_intent.active_attempt.get_id().clone(); @@ -112,35 +93,12 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let token = token.or_else(|| payment_attempt.payment_token.clone()); - - helpers::validate_pm_or_token_given( - &request.payment_method, - &request.payment_method_data, - &request.payment_method_type, - &mandate_type, - &token, - )?; - - payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); - payment_attempt.browser_info = browser_info; - payment_attempt.payment_method_type = - payment_method_type.or(payment_attempt.payment_method_type); - payment_attempt.payment_experience = request.payment_experience; currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); - - helpers::validate_customer_id_mandatory_cases( - request.setup_future_usage.is_some(), - &payment_intent - .customer_id - .clone() - .or_else(|| request.customer_id.clone()), - )?; + amount = payment_attempt.get_total_amount().into(); let shipping_address = helpers::create_or_find_address_for_payment_by_request( db, - request.shipping.as_ref(), + None, payment_intent.shipping_address_id.as_deref(), merchant_id, payment_intent.customer_id.as_ref(), @@ -151,7 +109,7 @@ impl .await?; let billing_address = helpers::create_or_find_address_for_payment_by_request( db, - request.billing.as_ref(), + None, payment_intent.billing_address_id.as_deref(), merchant_id, payment_intent.customer_id.as_ref(), @@ -161,43 +119,8 @@ impl ) .await?; - let redirect_response = request - .feature_metadata - .as_ref() - .and_then(|fm| fm.redirect_response.clone()); - payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); - payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string()); - - payment_intent.allowed_payment_method_types = request - .get_allowed_payment_method_types_as_value() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error converting allowed_payment_types to Value")? - .or(payment_intent.allowed_payment_method_types); - - payment_intent.connector_metadata = request - .get_connector_metadata_as_value() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error converting connector_metadata to Value")? - .or(payment_intent.connector_metadata); - - payment_intent.feature_metadata = request - .get_feature_metadata_as_value() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error converting feature_metadata to Value")? - .or(payment_intent.feature_metadata); - - payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata); - - // The operation merges mandate data from both request and payment_attempt - let setup_mandate = setup_mandate.map(|mandate_data| MandateData { - customer_acceptance: mandate_data.customer_acceptance, - mandate_type: payment_attempt - .mandate_details - .clone() - .or(mandate_data.mandate_type), - }); let frm_response = db .find_fraud_check_by_payment_id(payment_intent.payment_id.clone(), merchant_account.merchant_id.clone()) @@ -207,131 +130,58 @@ 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: 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()), }, - 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: 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, + incremental_authorization_details: None, + authorizations: vec![], + frm_metadata: None, + }; -#[async_trait] -impl Domain - for PaymentApprove -{ - #[instrument(skip_all)] - async fn get_or_create_customer_details<'a>( - &'a self, - db: &dyn StorageInterface, - payment_data: &mut PaymentData, - request: Option, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult< - ( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - Option, - ), - errors::StorageError, - > { - helpers::create_customer_if_not_exist( - Box::new(self), - db, + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, payment_data, - request, - &key_store.merchant_id, - key_store, - ) - .await - } - - #[instrument(skip_all)] - async fn make_pm_data<'a>( - &'a self, - state: &'a AppState, - payment_data: &mut PaymentData, - _storage_scheme: storage_enums::MerchantStorageScheme, - merchant_key_store: &domain::MerchantKeyStore, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - Option, - )> { - let (op, payment_method_data) = - helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await?; - - utils::when(payment_method_data.is_none(), || { - Err(errors::ApiErrorResponse::PaymentMethodNotFound) - })?; - - Ok((op, payment_method_data)) - } - - #[instrument(skip_all)] - async fn add_task_to_process_tracker<'a>( - &'a self, - _state: &'a AppState, - _payment_attempt: &storage::PaymentAttempt, - _requeue: bool, - _schedule_time: Option, - ) -> CustomResult<(), errors::ApiErrorResponse> { - Ok(()) - } + business_profile, + }; - async fn get_connector<'a>( - &'a self, - _merchant_account: &domain::MerchantAccount, - state: &AppState, - request: &api::PaymentsRequest, - _payment_intent: &storage::PaymentIntent, - _key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - // Use a new connector in the confirm call or use the same one which was passed when - // creating the payment or if none is passed then use the routing algorithm - helpers::get_connector_default(state, request.routing.clone()).await + Ok(get_trackers_response) } } #[async_trait] impl - UpdateTracker, api::PaymentsRequest, Ctx> for PaymentApprove + UpdateTracker, api::PaymentsCaptureRequest, Ctx> for PaymentApprove { #[instrument(skip_all)] async fn update_trackers<'b>( @@ -345,7 +195,7 @@ impl _frm_suggestion: Option, _header_payload: api::HeaderPayload, ) -> RouterResult<( - BoxedOperation<'b, F, api::PaymentsRequest, Ctx>, + BoxedOperation<'b, F, api::PaymentsCaptureRequest, Ctx>, PaymentData, )> where @@ -369,27 +219,18 @@ impl } } -impl ValidateRequest - for PaymentApprove +impl + ValidateRequest for PaymentApprove { #[instrument(skip_all)] fn validate_request<'a, 'b>( &'b self, - request: &api::PaymentsRequest, + request: &api::PaymentsCaptureRequest, merchant_account: &'a domain::MerchantAccount, ) -> RouterResult<( - BoxedOperation<'b, F, api::PaymentsRequest, Ctx>, + BoxedOperation<'b, F, api::PaymentsCaptureRequest, Ctx>, 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 { @@ -397,23 +238,17 @@ impl ValidateRequest merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsCancelRequest, Ctx>, - PaymentData, - Option, - )> { + _payment_confirm_source: Option, + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -106,7 +103,7 @@ impl .await?; let currency = payment_attempt.currency.get_required_value("currency")?; - let amount = payment_attempt.amount.into(); + let amount = payment_attempt.get_total_amount().into(); payment_attempt.cancellation_reason = request.cancellation_reason.clone(); @@ -128,45 +125,66 @@ 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")?; - 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 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, + 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, + incremental_authorization_details: None, + authorizations: vec![], + frm_metadata: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -198,6 +216,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 { diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index ff51a2c49d77..acf2ce431195 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -24,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] @@ -41,11 +41,8 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsCaptureRequest, Ctx>, - payments::PaymentData, - Option, - )> { + _payment_confirm_source: Option, + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -128,7 +125,7 @@ impl currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); + amount = payment_attempt.get_total_amount().into(); let shipping_address = helpers::create_or_find_address_for_payment_by_request( db, @@ -172,44 +169,66 @@ 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, + incremental_authorization_details: None, + authorizations: vec![], + frm_metadata: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -236,20 +255,29 @@ impl where F: 'b + Send, { - payment_data.payment_attempt = match &payment_data.multiple_capture_data { - Some(multiple_capture_data) => db - .store + 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 c648d95a4950..b4f538e1d089 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,8 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + _payment_confirm_source: Option, + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -135,9 +132,11 @@ impl payment_attempt.browser_info = browser_info; payment_attempt.payment_method_type = payment_method_type.or(payment_attempt.payment_method_type); - payment_attempt.payment_experience = request.payment_experience; + payment_attempt.payment_experience = request + .payment_experience + .or(payment_attempt.payment_experience); currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); + amount = payment_attempt.get_total_amount().into(); helpers::validate_customer_id_mandatory_cases( request.setup_future_usage.is_some(), @@ -177,7 +176,11 @@ impl payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); - payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string()); + payment_intent.return_url = request + .return_url + .as_ref() + .map(|a| a.to_string()) + .or(payment_intent.return_url); payment_intent.allowed_payment_method_types = request .get_allowed_payment_method_types_as_value() @@ -202,50 +205,74 @@ 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, + incremental_authorization_details: None, + authorizations: vec![], + frm_metadata: 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) } } @@ -285,12 +312,19 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { - let (op, payment_method_data) = - helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await?; + let (op, payment_method_data) = helpers::make_pm_data( + Box::new(self), + state, + payment_data, + merchant_key_store, + customer, + ) + .await?; Ok((op, payment_method_data)) } @@ -357,14 +391,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 +407,14 @@ impl ValidateRequest @@ -50,12 +56,8 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { - let db = &*state.store; + payment_confirm_source: Option, + ) -> RouterResult> { let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; let (currency, amount); @@ -65,7 +67,6 @@ 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( @@ -107,39 +108,73 @@ impl utils::flatten_join_error(mandate_details_fut) )?; - helpers::validate_customer_access(&payment_intent, auth_flow, request)?; + if let Some(order_details) = &request.order_details { + helpers::validate_order_details_amount( + order_details.to_owned(), + payment_intent.amount, + false, + )?; + } - helpers::validate_payment_status_against_not_allowed_statuses( - &payment_intent.status, - &[ - storage_enums::IntentStatus::Cancelled, - storage_enums::IntentStatus::Succeeded, - storage_enums::IntentStatus::Processing, - storage_enums::IntentStatus::RequiresCapture, - storage_enums::IntentStatus::RequiresMerchantAction, - ], - "confirm", - )?; + helpers::validate_customer_access(&payment_intent, auth_flow, request)?; - let intent_fulfillment_time = helpers::get_merchant_fullfillment_time( - payment_intent.payment_link_id.clone(), - merchant_account.intent_fulfillment_time, - db, - ) - .await?; + if let Some(common_enums::PaymentSource::Webhook) = payment_confirm_source { + helpers::validate_payment_status_against_not_allowed_statuses( + &payment_intent.status, + &[ + storage_enums::IntentStatus::Cancelled, + storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::Processing, + storage_enums::IntentStatus::RequiresCapture, + storage_enums::IntentStatus::RequiresMerchantAction, + ], + "confirm", + )?; + } else { + helpers::validate_payment_status_against_not_allowed_statuses( + &payment_intent.status, + &[ + storage_enums::IntentStatus::Cancelled, + storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::Processing, + storage_enums::IntentStatus::RequiresCapture, + storage_enums::IntentStatus::RequiresMerchantAction, + storage_enums::IntentStatus::RequiresCustomerAction, + ], + "confirm", + )?; + } - helpers::authenticate_client_secret( - request.client_secret.as_ref(), - &payment_intent, - intent_fulfillment_time, - )?; + helpers::authenticate_client_secret(request.client_secret.as_ref(), &payment_intent)?; let customer_details = helpers::get_customer_details_from_request(request); // Stage 2 - let attempt_id = payment_intent.active_attempt.get_id(); - let store = state.clone().store; + 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.store.clone(); + let m_payment_id = payment_intent.payment_id.clone(); let m_merchant_id = merchant_id.clone(); @@ -169,7 +204,7 @@ impl let shipping_address_fut = tokio::spawn( async move { - helpers::create_or_find_address_for_payment_by_request( + 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(), @@ -197,7 +232,7 @@ impl let billing_address_fut = tokio::spawn( async move { - helpers::create_or_find_address_for_payment_by_request( + 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(), @@ -235,48 +270,72 @@ impl .in_current_span(), ); - 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, _) = 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(config_update_fut) - )?; - - (payment_attempt, shipping_address, billing_address) - } - _ => { - let (mut payment_attempt, shipping_address, billing_address, _) = 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(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( - request, - payment_intent, + // 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, - &*state.store, - 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() @@ -331,7 +390,7 @@ impl payment_attempt.capture_method = request.capture_method.or(payment_attempt.capture_method); currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); + amount = payment_attempt.get_total_amount().into(); helpers::validate_customer_id_mandatory_cases( request.setup_future_usage.is_some(), @@ -372,6 +431,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() @@ -379,59 +447,93 @@ impl // The operation merges mandate data from both request and payment_attempt setup_mandate = setup_mandate.map(|mut sm| { - sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); + sm.mandate_type = payment_attempt + .mandate_details + .clone() + .and_then(|mandate| match mandate { + data_models::mandates::MandateTypeDetails::MandateType(mandate_type) => { + Some(mandate_type) + } + data_models::mandates::MandateTypeDetails::MandateDetails(mandate_details) => { + mandate_details.mandate_type + } + }) + .or(sm.mandate_type); + sm.update_mandate_id = payment_attempt + .mandate_details + .clone() + .and_then(|mandate| match mandate { + data_models::mandates::MandateTypeDetails::MandateType(_) => None, + data_models::mandates::MandateTypeDetails::MandateDetails(update_id) => { + Some(update_id.update_mandate_id) + } + }) + .flatten() + .or(sm.update_mandate_id); sm }); - Self::validate_request_surcharge_details_with_session_surcharge_details( - state, - &payment_attempt, - request, - ) - .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 additional_pm_data = request + .payment_method_data + .as_ref() + .async_map(|payment_method_data| async { + helpers::get_additional_payment_data(payment_method_data, &*state.store).await + }) + .await; + let payment_method_data_after_card_bin_call = request + .payment_method_data + .as_ref() + .zip(additional_pm_data) + .map(|(payment_method_data, additional_payment_data)| { + payment_method_data.apply_additional_payment_data(additional_payment_data) + }); + + 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: payment_method_data_after_card_bin_call, + 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, + incremental_authorization_details: None, + authorizations: vec![], + frm_metadata: request.frm_metadata.clone(), + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -471,12 +573,13 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { let (op, payment_method_data) = - helpers::make_pm_data(Box::new(self), state, payment_data, key_store).await?; + helpers::make_pm_data(Box::new(self), state, payment_data, key_store, customer).await?; utils::when(payment_method_data.is_none(), || { Err(errors::ApiErrorResponse::PaymentMethodNotFound) @@ -528,6 +631,16 @@ impl Domain( + &'a self, + state: &AppState, + payment_data: &mut PaymentData, + _merchant_account: &domain::MerchantAccount, + ) -> CustomResult<(), errors::ApiErrorResponse> { + populate_surcharge_details(state, payment_data).await + } } #[async_trait] @@ -552,32 +665,34 @@ impl where F: 'b + Send, { + let db = state.store.as_ref(); let payment_method = payment_data.payment_attempt.payment_method; let browser_info = payment_data.payment_attempt.browser_info.clone(); let frm_message = payment_data.frm_message.clone(); - let (intent_status, attempt_status, (error_code, error_message)) = match frm_suggestion { - Some(FrmSuggestion::FrmCancelTransaction) => ( - storage_enums::IntentStatus::Failed, - storage_enums::AttemptStatus::Failure, - frm_message.map_or((None, None), |fraud_check| { - ( - Some(Some(fraud_check.frm_status.to_string())), - Some(fraud_check.frm_reason.map(|reason| reason.to_string())), - ) - }), - ), - Some(FrmSuggestion::FrmManualReview) => ( - storage_enums::IntentStatus::RequiresMerchantAction, - storage_enums::AttemptStatus::Unresolved, - (None, None), - ), - _ => ( - storage_enums::IntentStatus::Processing, - storage_enums::AttemptStatus::Pending, - (None, None), - ), - }; + let (mut intent_status, mut attempt_status, (error_code, error_message)) = + match frm_suggestion { + Some(FrmSuggestion::FrmCancelTransaction) => ( + storage_enums::IntentStatus::Failed, + storage_enums::AttemptStatus::Failure, + frm_message.map_or((None, None), |fraud_check| { + ( + Some(Some(fraud_check.frm_status.to_string())), + Some(fraud_check.frm_reason.map(|reason| reason.to_string())), + ) + }), + ), + Some(FrmSuggestion::FrmManualReview) => ( + storage_enums::IntentStatus::RequiresMerchantAction, + storage_enums::AttemptStatus::Unresolved, + (None, None), + ), + _ => ( + storage_enums::IntentStatus::Processing, + storage_enums::AttemptStatus::Pending, + (None, None), + ), + }; let connector = payment_data.payment_attempt.connector.clone(); let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); @@ -641,12 +756,172 @@ impl let m_error_message = error_message.clone(); let m_db = state.clone().store; + // Validate Blocklist + let merchant_id = payment_data.payment_attempt.merchant_id; + let merchant_fingerprint_secret = + blocklist_utils::get_merchant_fingerprint_secret(state, &merchant_id).await?; + + // Hashed Fingerprint to check whether or not this payment should be blocked. + let card_number_fingerprint = payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api_models::payments::PaymentMethodData::Card(card) => { + crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_fingerprint_secret.as_bytes(), + card.card_number.clone().get_card_no().as_bytes(), + ) + .attach_printable("error in pm fingerprint creation") + .map_or_else( + |err| { + logger::error!(error=?err); + None + }, + Some, + ) + } + _ => None, + }) + .map(hex::encode); + + // Hashed Cardbin to check whether or not this payment should be blocked. + let card_bin_fingerprint = payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api_models::payments::PaymentMethodData::Card(card) => { + crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_fingerprint_secret.as_bytes(), + card.card_number.clone().get_card_isin().as_bytes(), + ) + .attach_printable("error in card bin hash creation") + .map_or_else( + |err| { + logger::error!(error=?err); + None + }, + Some, + ) + } + _ => None, + }) + .map(hex::encode); + + // Hashed Extended Cardbin to check whether or not this payment should be blocked. + let extended_card_bin_fingerprint = payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api_models::payments::PaymentMethodData::Card(card) => { + crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_fingerprint_secret.as_bytes(), + card.card_number.clone().get_extended_card_bin().as_bytes(), + ) + .attach_printable("error in extended card bin hash creation") + .map_or_else( + |err| { + logger::error!(error=?err); + None + }, + Some, + ) + } + _ => None, + }) + .map(hex::encode); + + let mut fingerprint_id = None; + + //validating the payment method. + let mut is_pm_blocklisted = false; + + let mut blocklist_futures = Vec::new(); + if let Some(card_number_fingerprint) = card_number_fingerprint.as_ref() { + blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &merchant_id, + card_number_fingerprint, + )); + } + + if let Some(card_bin_fingerprint) = card_bin_fingerprint.as_ref() { + blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &merchant_id, + card_bin_fingerprint, + )); + } + + if let Some(extended_card_bin_fingerprint) = extended_card_bin_fingerprint.as_ref() { + blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &merchant_id, + extended_card_bin_fingerprint, + )); + } + + let blocklist_lookups = futures::future::join_all(blocklist_futures).await; + + if blocklist_lookups.iter().any(|x| x.is_ok()) { + intent_status = storage_enums::IntentStatus::Failed; + attempt_status = storage_enums::AttemptStatus::Failure; + is_pm_blocklisted = true; + } + + if let Some(encoded_hash) = card_number_fingerprint { + #[cfg(feature = "kms")] + let encrypted_fingerprint = kms::get_kms_client(&state.conf.kms) + .await + .encrypt(encoded_hash) + .await + .map_or_else( + |e| { + logger::error!(error=?e, "failed kms encryption of card fingerprint"); + None + }, + Some, + ); + + #[cfg(not(feature = "kms"))] + let encrypted_fingerprint = Some(encoded_hash); + + if let Some(encrypted_fingerprint) = encrypted_fingerprint { + fingerprint_id = db + .insert_blocklist_fingerprint_entry( + diesel_models::blocklist_fingerprint::BlocklistFingerprintNew { + merchant_id, + fingerprint_id: utils::generate_id(consts::ID_LENGTH, "fingerprint"), + encrypted_fingerprint, + data_kind: common_enums::BlocklistDataKind::PaymentMethod, + created_at: common_utils::date_time::now(), + }, + ) + .await + .map_or_else( + |e| { + logger::error!(error=?e, "failed storing card fingerprint in db"); + None + }, + |fp| Some(fp.fingerprint_id), + ); + } + } + + 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(), + amount: payment_data.payment_attempt.amount, currency: payment_data.currency, status: attempt_status, payment_method, @@ -664,6 +939,8 @@ impl amount_capturable: Some(authorized_amount), updated_by: storage_scheme.to_string(), merchant_connector_id, + surcharge_amount, + tax_amount, }, storage_scheme, ) @@ -686,13 +963,14 @@ impl let m_metadata = metadata.clone(); let m_db = state.clone().store; let m_storage_scheme = storage_scheme.to_string(); + let session_expiry = m_payment_data_payment_intent.session_expiry; 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(), + amount: payment_data.payment_intent.amount, currency: payment_data.currency, setup_future_usage, status: intent_status, @@ -709,6 +987,8 @@ impl metadata: m_metadata, payment_confirm_source: header_payload.payment_confirm_source, updated_by: m_storage_scheme, + fingerprint_id, + session_expiry, }, storage_scheme, ) @@ -757,6 +1037,11 @@ impl payment_data.payment_intent = payment_intent; payment_data.payment_attempt = payment_attempt; + // Block the payment if the entry was present in the Blocklist + if is_pm_blocklisted { + return Err(errors::ApiErrorResponse::PaymentBlocked.into()); + } + Ok((Box::new(self), payment_data)) } } @@ -774,14 +1059,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) @@ -794,14 +1071,19 @@ impl ValidateRequest ValidateRequest RouterResult<()> { - match ( - request.surcharge_details, - request.payment_method_data.as_ref(), - ) { - (Some(request_surcharge_details), Some(payment_method_data)) => { - if let Some(payment_method_type) = - payment_method_data.get_payment_method_type_if_session_token_type() - { - let invalid_surcharge_details_error = Err(errors::ApiErrorResponse::InvalidRequestData { - message: "surcharge_details sent in session token flow doesn't match with the one sent in confirm request".into(), - }.into()); - if let Some(attempt_surcharge_amount) = payment_attempt.surcharge_amount { - // payment_attempt.surcharge_amount will be Some if some surcharge was sent in payment create - // if surcharge was sent in payment create call, the same would have been sent to the connector during session call - // So verify the same - if request_surcharge_details.surcharge_amount != attempt_surcharge_amount - || request_surcharge_details.tax_amount != payment_attempt.tax_amount - { - return invalid_surcharge_details_error; - } - } else { - // if not sent in payment create - // verify that any calculated surcharge sent in session flow is same as the one sent in confirm - return match get_individual_surcharge_detail_from_redis( - state, - &payment_method_type.into(), - &payment_method_type, - None, - &payment_attempt.attempt_id, - ) - .await - { - Ok(surcharge_details) => utils::when( - !surcharge_details - .is_request_surcharge_matching(request_surcharge_details), - || invalid_surcharge_details_error, - ), - Err(err) if err.current_context() == &RedisError::NotFound => { - utils::when(!request_surcharge_details.is_surcharge_zero(), || { - invalid_surcharge_details_error - }) - } - Err(err) => Err(err) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch redis value"), - }; - } - } - Ok(()) - } - (Some(_request_surcharge_details), None) => { - Err(errors::ApiErrorResponse::MissingRequiredField { - field_name: "payment_method_data", - } - .into()) - } - _ => 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 974f5e6ab5b6..381f02659769 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1,19 +1,25 @@ 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}; +use data_models::{ + mandates::{MandateData, MandateDetails, MandateTypeDetails}, + payments::payment_attempt::PaymentAttempt, +}; use diesel_models::ephemeral_key; use error_stack::{self, ResultExt}; +use masking::PeekInterface; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; +use time::PrimitiveDateTime; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ consts, core::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, + payment_link, payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, utils as core_utils, @@ -34,7 +40,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 +59,8 @@ impl merchant_account: &domain::MerchantAccount, merchant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + _payment_confirm_source: 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; @@ -70,30 +73,36 @@ impl .get_payment_intent_id() .change_context(errors::ApiErrorResponse::PaymentNotFound)?; - let payment_link_data = if let Some(payment_link_object) = &request.payment_link_object { - create_payment_link( - request, - payment_link_object.clone(), - merchant_id.clone(), - payment_id.clone(), - db, - state, - amount, - ) - .await? - } else { - None - }; - helpers::validate_business_details( request.business_country, request.business_label.as_ref(), merchant_account, )?; + // If profile id is not passed, get it from the business_country and business_label + let profile_id = core_utils::get_profile_id_from_business_details( + request.business_country, + request.business_label.as_ref(), + merchant_account, + request.profile_id.as_ref(), + &*state.store, + true, + ) + .await?; + // Validate whether profile_id passed in request is valid and is linked to the merchant - core_utils::validate_and_get_business_profile(db, request.profile_id.as_ref(), merchant_id) - .await?; + let business_profile = if let Some(business_profile) = + core_utils::validate_and_get_business_profile(db, Some(&profile_id), merchant_id) + .await? + { + business_profile + } else { + db.find_business_profile_by_profile_id(&profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })? + }; let ( token, @@ -157,6 +166,52 @@ impl utils::get_payment_attempt_id(payment_id.clone(), 1) }; + let session_expiry = + common_utils::date_time::now().saturating_add(time::Duration::seconds( + request.session_expiry.map(i64::from).unwrap_or( + business_profile + .session_expiry + .unwrap_or(consts::DEFAULT_SESSION_EXPIRY), + ), + )); + + let payment_link_data = if let Some(payment_link_create) = request.payment_link { + if payment_link_create { + let merchant_name = merchant_account + .merchant_name + .clone() + .map(|merchant_name| merchant_name.into_inner().peek().to_owned()) + .unwrap_or_default(); + + let default_domain_name = state.conf.server.base_url.clone(); + + let (payment_link_config, domain_name) = + payment_link::get_payment_link_config_based_on_priority( + request.payment_link_config.clone(), + business_profile.payment_link_config.clone(), + merchant_name, + default_domain_name, + )?; + create_payment_link( + request, + payment_link_config, + merchant_id.clone(), + payment_id.clone(), + db, + amount, + request.description.clone(), + profile_id.clone(), + domain_name, + session_expiry, + ) + .await? + } else { + None + } + } else { + None + }; + let payment_intent_new = Self::make_payment_intent( &payment_id, merchant_account, @@ -166,11 +221,12 @@ impl payment_link_data.clone(), billing_address.clone().map(|x| x.address_id), attempt_id, - state, + profile_id, + session_expiry, ) .await?; - let payment_attempt_new = Self::make_payment_attempt( + let (payment_attempt_new, additional_payment_data) = Self::make_payment_attempt( &payment_id, merchant_id, money, @@ -189,13 +245,21 @@ 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, + false, + )?; + } + payment_attempt = db .insert_payment_attempt(payment_attempt_new, storage_scheme) .await .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { payment_id: payment_id.clone(), })?; - + // connector mandate reference update history let mandate_id = request .mandate_id .as_ref() @@ -224,10 +288,11 @@ impl api_models::payments::MandateIds { mandate_id: mandate_obj.mandate_id, mandate_reference_id: Some(api_models::payments::MandateReferenceId::ConnectorMandateId( - api_models::payments::ConnectorMandateReferenceId { - connector_mandate_id: connector_id.connector_mandate_id, - payment_method_id: connector_id.payment_method_id, - }, + api_models::payments::ConnectorMandateReferenceId{ + connector_mandate_id: connector_id.connector_mandate_id, + payment_method_id: connector_id.payment_method_id, + update_history: None + } )) } }), @@ -246,6 +311,7 @@ impl request.confirm, self, ); + let creds_identifier = request .merchant_connector_details .as_ref() @@ -265,59 +331,66 @@ impl .transpose()?; // The operation merges mandate data from both request and payment_attempt - let setup_mandate: Option = setup_mandate.map(Into::into); - - // 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), - } + let setup_mandate = setup_mandate.map(MandateData::from); + + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { + payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); - 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_method_data_after_card_bin_call = request + .payment_method_data + .as_ref() + .zip(additional_payment_data) + .map(|(payment_method_data, additional_payment_data)| { + payment_method_data.apply_additional_payment_data(additional_payment_data) + }); + let amount = payment_attempt.get_total_amount().into(); + 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: payment_method_data_after_card_bin_call, + 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, + incremental_authorization_details: None, + authorizations: vec![], + frm_metadata: request.frm_metadata.clone(), + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation, + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -357,11 +430,19 @@ impl Domain, _storage_scheme: enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await + helpers::make_pm_data( + Box::new(self), + state, + payment_data, + merchant_key_store, + customer, + ) + .await } #[instrument(skip_all)] @@ -370,7 +451,7 @@ impl Domain, + _schedule_time: Option, ) -> CustomResult<(), errors::ApiErrorResponse> { Ok(()) } @@ -505,24 +586,20 @@ impl ValidateRequest, )> { helpers::validate_customer_details_in_request(request)?; - - if let Some(payment_link_object) = &request.payment_link_object { - helpers::validate_payment_link_request( - payment_link_object, - request.confirm, - request.order_details.clone(), - )?; + if let Some(session_expiry) = &request.session_expiry { + helpers::validate_session_expiry(session_expiry.to_owned())?; } - 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, + if let Some(payment_link) = &request.payment_link { + if *payment_link { + helpers::validate_payment_link_request(request.confirm)?; + } }; + 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) .change_context(errors::ApiErrorResponse::MerchantAccountNotFound)?; @@ -530,18 +607,18 @@ impl ValidateRequest ValidateRequest, state: &AppState, - ) -> RouterResult { + ) -> RouterResult<( + storage::PaymentAttemptNew, + Option, + )> { let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); let status = helpers::payment_attempt_status_fsm(&request.payment_method_data, request.confirm); @@ -604,7 +684,8 @@ impl PaymentCreate { .async_map(|payment_method_data| async { helpers::get_additional_payment_data(payment_method_data, &*state.store).await }) - .await + .await; + let additional_pm_data_value = additional_pm_data .as_ref() .map(Encode::::encode_to_value) .transpose() @@ -618,36 +699,73 @@ impl PaymentCreate { } else { utils::get_payment_attempt_id(payment_id, 1) }; + let surcharge_amount = request + .surcharge_details + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = request + .surcharge_details + .and_then(|surcharge_details| surcharge_details.tax_amount); - Ok(storage::PaymentAttemptNew { - payment_id: payment_id.to_string(), - merchant_id: merchant_id.to_string(), - attempt_id, - status, - currency, - amount: amount.into(), - payment_method, - capture_method: request.capture_method, - capture_on: request.capture_on, - confirm: request.confirm.unwrap_or(false), - created_at, - modified_at, - last_synced, - authentication_type: request.authentication_type, - browser_info, - payment_experience: request.payment_experience, - payment_method_type, - payment_method_data: additional_pm_data, - amount_to_capture: request.amount_to_capture, - payment_token: request.payment_token.clone(), - mandate_id: request.mandate_id.clone(), - business_sub_label: request.business_sub_label.clone(), - mandate_details: request - .mandate_data - .as_ref() - .and_then(|inner| inner.mandate_type.clone().map(Into::into)), - ..storage::PaymentAttemptNew::default() - }) + if request.mandate_data.as_ref().map_or(false, |mandate_data| { + mandate_data.update_mandate_id.is_some() && mandate_data.mandate_type.is_some() + }) { + Err(errors::ApiErrorResponse::InvalidRequestData {message:"Only one field out of 'mandate_type' and 'update_mandate_id' was expected, found both".to_string()})? + } + + let mandate_dets = if let Some(update_id) = request + .mandate_data + .as_ref() + .and_then(|inner| inner.update_mandate_id.clone()) + { + let mandate_data = MandateDetails { + update_mandate_id: Some(update_id), + mandate_type: None, + }; + Some(MandateTypeDetails::MandateDetails(mandate_data)) + } else { + // let mandate_type: data_models::mandates::MandateDataType = + + let mandate_data = MandateDetails { + update_mandate_id: None, + mandate_type: request + .mandate_data + .as_ref() + .and_then(|inner| inner.mandate_type.clone().map(Into::into)), + }; + Some(MandateTypeDetails::MandateDetails(mandate_data)) + }; + + Ok(( + storage::PaymentAttemptNew { + payment_id: payment_id.to_string(), + merchant_id: merchant_id.to_string(), + attempt_id, + status, + currency, + amount: amount.into(), + payment_method, + capture_method: request.capture_method, + capture_on: request.capture_on, + confirm: request.confirm.unwrap_or(false), + created_at, + modified_at, + last_synced, + authentication_type: request.authentication_type, + browser_info, + payment_experience: request.payment_experience, + payment_method_type, + payment_method_data: additional_pm_data_value, + amount_to_capture: request.amount_to_capture, + payment_token: request.payment_token.clone(), + mandate_id: request.mandate_id.clone(), + business_sub_label: request.business_sub_label.clone(), + surcharge_amount, + tax_amount, + mandate_details: mandate_dets, + ..storage::PaymentAttemptNew::default() + }, + additional_pm_data, + )) } #[instrument(skip_all)] @@ -661,7 +779,8 @@ impl PaymentCreate { payment_link_data: Option, billing_address_id: Option, active_attempt_id: String, - state: &AppState, + profile_id: String, + session_expiry: PrimitiveDateTime, ) -> RouterResult { let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); let status = @@ -675,17 +794,6 @@ impl PaymentCreate { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to convert order details to value")?; - // If profile id is not passed, get it from the business_country and business_label - let profile_id = core_utils::get_profile_id_from_business_details( - request.business_country, - request.business_label.as_ref(), - merchant_account, - request.profile_id.as_ref(), - &*state.store, - true, - ) - .await?; - let allowed_payment_method_types = request .get_allowed_payment_method_types_as_value() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -703,6 +811,12 @@ impl PaymentCreate { let payment_link_id = payment_link_data.map(|pl_data| pl_data.payment_link_id); + let request_incremental_authorization = + core_utils::get_request_incremental_authorization_value( + request.request_incremental_authorization, + request.capture_method, + )?; + Ok(storage::PaymentIntentNew { payment_id: payment_id.to_string(), merchant_id: merchant_account.merchant_id.to_string(), @@ -739,6 +853,11 @@ impl PaymentCreate { payment_confirm_source: None, surcharge_applicable: None, updated_by: merchant_account.storage_scheme.to_string(), + request_incremental_authorization, + incremental_authorization_allowed: None, + authorization_count: None, + fingerprint_id: None, + session_expiry: Some(session_expiry), }) } @@ -777,29 +896,35 @@ pub fn payments_create_request_validation( Ok((amount, currency)) } +#[allow(clippy::too_many_arguments)] async fn create_payment_link( request: &api::PaymentsRequest, - payment_link_object: api_models::payments::PaymentLinkObject, + payment_link_config: api_models::admin::PaymentLinkConfig, merchant_id: String, payment_id: String, db: &dyn StorageInterface, - state: &AppState, amount: api::Amount, + description: Option, + profile_id: String, + domain_name: String, + session_expiry: PrimitiveDateTime, ) -> 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 { - format!("https://{domain_name}") - } else { - state.conf.server.base_url.clone() - }; - let payment_link_id = utils::generate_id(consts::ID_LENGTH, "plink"); let payment_link = format!( "{}/payment_link/{}/{}", - domain, + domain_name, merchant_id.clone(), payment_id.clone() ); + + let payment_link_config_encoded_value = common_utils::ext_traits::Encode::< + api_models::admin::PaymentLinkConfig, + >::encode_to_value(&payment_link_config) + .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(), @@ -809,8 +934,11 @@ async fn create_payment_link( currency: request.currency, created_at, last_modified_at, - fulfilment_time: payment_link_object.link_expiry, - custom_merchant_name: payment_link_object.custom_merchant_name, + fulfilment_time: Some(session_expiry), + custom_merchant_name: Some(payment_link_config.seller_name), + description, + payment_link_config: Some(payment_link_config_encoded_value), + profile_id: Some(profile_id), }; let payment_link_db = db .insert_payment_link(payment_link_req) diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs deleted file mode 100644 index 62f12cfbc90c..000000000000 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ /dev/null @@ -1,397 +0,0 @@ -use std::marker::PhantomData; - -use api_models::enums::FrmSuggestion; -use async_trait::async_trait; -use common_utils::{date_time, errors::CustomResult, ext_traits::AsyncExt}; -use error_stack::ResultExt; -use router_derive::PaymentOperation; -use router_env::{instrument, tracing}; - -use super::{BoxedOperation, Domain, GetTracker, UpdateTracker, ValidateRequest}; -use crate::{ - consts, - core::{ - errors::{self, RouterResult, StorageErrorExt}, - payment_methods::PaymentMethodRetrieve, - payments::{self, helpers, operations, Operation, PaymentData}, - utils as core_utils, - }, - db::StorageInterface, - routes::AppState, - services, - types::{ - self, - api::{self, enums as api_enums, PaymentIdTypeExt}, - domain, - storage::{self, enums as storage_enums}, - }, - utils, -}; - -#[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "verify")] -pub struct PaymentMethodValidate; - -impl ValidateRequest - for PaymentMethodValidate -{ - #[instrument(skip_all)] - fn validate_request<'a, 'b>( - &'b self, - request: &api::VerifyRequest, - merchant_account: &'a domain::MerchantAccount, - ) -> RouterResult<( - BoxedOperation<'b, F, api::VerifyRequest, Ctx>, - operations::ValidateResult<'a>, - )> { - let request_merchant_id = request.merchant_id.as_deref(); - helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) - .change_context(errors::ApiErrorResponse::MerchantAccountNotFound)?; - - let mandate_type = - helpers::validate_mandate(request, payments::is_operation_confirm(self))?; - let validation_id = core_utils::get_or_generate_id("validation_id", &None, "val")?; - - Ok(( - Box::new(self), - operations::ValidateResult { - merchant_id: &merchant_account.merchant_id, - payment_id: api::PaymentIdType::PaymentIntentId(validation_id), - mandate_type, - storage_scheme: merchant_account.storage_scheme, - requeue: false, - }, - )) - } -} - -#[async_trait] -impl - GetTracker, api::VerifyRequest, Ctx> for PaymentMethodValidate -{ - #[instrument(skip_all)] - async fn get_trackers<'a>( - &'a self, - state: &'a AppState, - payment_id: &api::PaymentIdType, - request: &api::VerifyRequest, - _mandate_type: Option, - merchant_account: &domain::MerchantAccount, - _mechant_key_store: &domain::MerchantKeyStore, - _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::VerifyRequest, Ctx>, - PaymentData, - Option, - )> { - let db = &*state.store; - - let merchant_id = &merchant_account.merchant_id; - let storage_scheme = merchant_account.storage_scheme; - - let (payment_intent, payment_attempt); - - let payment_id = payment_id - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting payment_intent_id from PaymentIdType")?; - - payment_attempt = match db - .insert_payment_attempt( - Self::make_payment_attempt( - &payment_id, - merchant_id, - request.payment_method, - request, - state, - merchant_account.storage_scheme, - ), - storage_scheme, - ) - .await - { - Ok(payment_attempt) => Ok(payment_attempt), - Err(err) => { - Err(err.change_context(errors::ApiErrorResponse::VerificationFailed { data: None })) - } - }?; - - payment_intent = match db - .insert_payment_intent( - Self::make_payment_intent( - &payment_id, - merchant_id, - request, - payment_attempt.attempt_id.clone(), - merchant_account.storage_scheme, - ), - storage_scheme, - ) - .await - { - Ok(payment_intent) => Ok(payment_intent), - Err(err) => { - Err(err.change_context(errors::ApiErrorResponse::VerificationFailed { data: None })) - } - }?; - - let creds_identifier = request - .merchant_connector_details - .as_ref() - .map(|mcd| mcd.creds_identifier.to_owned()); - 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, - ) - .await - }) - .await - .transpose()?; - - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - /// currency and amount are irrelevant in this scenario - currency: storage_enums::Currency::default(), - amount: api::Amount::Zero, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: request.mandate_data.clone().map(Into::into), - token: request.payment_token.clone(), - payment_method_data: request.payment_method_data.clone(), - confirm: Some(true), - address: types::PaymentAddress::default(), - 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, - }, - Some(payments::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(), - }), - )) - } -} - -#[async_trait] -impl UpdateTracker, api::VerifyRequest, Ctx> - for PaymentMethodValidate -{ - #[instrument(skip_all)] - async fn update_trackers<'b>( - &'b self, - state: &'b AppState, - mut payment_data: PaymentData, - _customer: Option, - storage_scheme: storage_enums::MerchantStorageScheme, - _updated_customer: Option, - _mechant_key_store: &domain::MerchantKeyStore, - _frm_suggestion: Option, - _header_payload: api::HeaderPayload, - ) -> RouterResult<( - BoxedOperation<'b, F, api::VerifyRequest, Ctx>, - PaymentData, - )> - where - F: 'b + Send, - { - // There is no fsm involved in this operation all the change of states must happen in a single request - let status = Some(storage_enums::IntentStatus::Processing); - - let customer_id = payment_data.payment_intent.customer_id.clone(); - - payment_data.payment_intent = state - .store - .update_payment_intent( - payment_data.payment_intent, - storage::PaymentIntentUpdate::ReturnUrlUpdate { - return_url: None, - status, - customer_id, - shipping_address_id: None, - billing_address_id: None, - updated_by: storage_scheme.to_string(), - }, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::VerificationFailed { data: None })?; - - Ok((Box::new(self), payment_data)) - } -} - -#[async_trait] -impl Domain for Op -where - F: Clone + Send, - Op: Send + Sync + Operation, - for<'a> &'a Op: Operation, -{ - #[instrument(skip_all)] - async fn get_or_create_customer_details<'a>( - &'a self, - db: &dyn StorageInterface, - payment_data: &mut PaymentData, - request: Option, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult< - ( - BoxedOperation<'a, F, api::VerifyRequest, Ctx>, - Option, - ), - errors::StorageError, - > { - helpers::create_customer_if_not_exist( - Box::new(self), - db, - payment_data, - request, - &key_store.merchant_id, - key_store, - ) - .await - } - - #[instrument(skip_all)] - async fn make_pm_data<'a>( - &'a self, - state: &'a AppState, - payment_data: &mut PaymentData, - _storage_scheme: storage_enums::MerchantStorageScheme, - merchant_key_store: &domain::MerchantKeyStore, - ) -> RouterResult<( - BoxedOperation<'a, F, api::VerifyRequest, Ctx>, - Option, - )> { - helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await - } - - async fn get_connector<'a>( - &'a self, - _merchant_account: &domain::MerchantAccount, - state: &AppState, - _request: &api::VerifyRequest, - _payment_intent: &storage::PaymentIntent, - _mechant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - helpers::get_connector_default(state, None).await - } -} - -impl PaymentMethodValidate { - #[instrument(skip_all)] - fn make_payment_attempt( - payment_id: &str, - merchant_id: &str, - payment_method: Option, - _request: &api::VerifyRequest, - state: &AppState, - storage_scheme: storage_enums::MerchantStorageScheme, - ) -> storage::PaymentAttemptNew { - let created_at @ modified_at @ last_synced = Some(date_time::now()); - let status = storage_enums::AttemptStatus::Pending; - let attempt_id = if core_utils::is_merchant_enabled_for_payment_id_as_connector_request_id( - &state.conf, - merchant_id, - ) { - payment_id.to_string() - } else { - utils::get_payment_attempt_id(payment_id, 1) - }; - - storage::PaymentAttemptNew { - payment_id: payment_id.to_string(), - merchant_id: merchant_id.to_string(), - attempt_id, - status, - // Amount & Currency will be zero in this case - amount: 0, - currency: Default::default(), - connector: None, - payment_method, - confirm: true, - created_at, - modified_at, - last_synced, - updated_by: storage_scheme.to_string(), - ..Default::default() - } - } - - fn make_payment_intent( - payment_id: &str, - merchant_id: &str, - request: &api::VerifyRequest, - active_attempt_id: String, - storage_scheme: storage_enums::MerchantStorageScheme, - ) -> storage::PaymentIntentNew { - let created_at @ modified_at @ last_synced = Some(date_time::now()); - let status = helpers::payment_intent_status_fsm(&request.payment_method_data, Some(true)); - - let client_secret = - utils::generate_id(consts::ID_LENGTH, format!("{payment_id}_secret").as_str()); - storage::PaymentIntentNew { - payment_id: payment_id.to_string(), - merchant_id: merchant_id.to_string(), - status, - amount: 0, - currency: Default::default(), - connector_id: None, - created_at, - modified_at, - last_synced, - client_secret: Some(client_secret), - setup_future_usage: request.setup_future_usage, - off_session: request.off_session, - active_attempt: data_models::RemoteStorageObject::ForeignID(active_attempt_id), - attempt_count: 1, - amount_captured: Default::default(), - customer_id: Default::default(), - description: Default::default(), - return_url: Default::default(), - metadata: Default::default(), - shipping_address_id: Default::default(), - billing_address_id: Default::default(), - statement_descriptor_name: Default::default(), - statement_descriptor_suffix: Default::default(), - business_country: Default::default(), - business_label: Default::default(), - order_details: Default::default(), - allowed_payment_method_types: Default::default(), - connector_metadata: Default::default(), - feature_metadata: Default::default(), - profile_id: Default::default(), - merchant_decision: Default::default(), - payment_confirm_source: Default::default(), - surcharge_applicable: Default::default(), - payment_link_id: Default::default(), - updated_by: storage_scheme.to_string(), - } - } -} diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 16d264c001ec..02d2e953c19c 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::{enums::FrmSuggestion, payments::PaymentsRejectRequest}; +use api_models::{enums::FrmSuggestion, payments::PaymentsCancelRequest}; use async_trait::async_trait; use error_stack::ResultExt; use router_derive; @@ -11,7 +11,7 @@ use crate::{ core::{ errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, }, routes::AppState, services, @@ -24,28 +24,25 @@ use crate::{ }; #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] -#[operation(ops = "all", flow = "reject")] +#[operation(operations = "all", flow = "cancel")] pub struct PaymentReject; #[async_trait] impl - GetTracker, PaymentsRejectRequest, Ctx> for PaymentReject + GetTracker, PaymentsCancelRequest, Ctx> for PaymentReject { #[instrument(skip_all)] async fn get_trackers<'a>( &'a self, state: &'a AppState, payment_id: &api::PaymentIdType, - _request: &PaymentsRejectRequest, + _request: &PaymentsCancelRequest, _mandate_type: Option, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, PaymentsRejectRequest, Ctx>, - PaymentData, - Option, - )> { + _payment_confirm_source: Option, + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -61,6 +58,7 @@ impl helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, &[ + enums::IntentStatus::Cancelled, enums::IntentStatus::Failed, enums::IntentStatus::Succeeded, enums::IntentStatus::Processing, @@ -104,7 +102,7 @@ impl .await?; let currency = payment_attempt.currency.get_required_value("currency")?; - let amount = payment_attempt.amount.into(); + let amount = payment_attempt.get_total_amount().into(); let frm_response = db .find_fraud_check_by_payment_id(payment_intent.payment_id.clone(), merchant_account.merchant_id.clone()) @@ -114,51 +112,73 @@ 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, + incremental_authorization_details: None, + authorizations: vec![], + frm_metadata: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } #[async_trait] impl - UpdateTracker, PaymentsRejectRequest, Ctx> for PaymentReject + UpdateTracker, PaymentsCancelRequest, Ctx> for PaymentReject { #[instrument(skip_all)] async fn update_trackers<'b>( @@ -172,7 +192,7 @@ impl _should_decline_transaction: Option, _header_payload: api::HeaderPayload, ) -> RouterResult<( - BoxedOperation<'b, F, PaymentsRejectRequest, Ctx>, + BoxedOperation<'b, F, PaymentsCancelRequest, Ctx>, PaymentData, )> where @@ -224,16 +244,16 @@ impl } } -impl ValidateRequest +impl ValidateRequest for PaymentReject { #[instrument(skip_all)] fn validate_request<'a, 'b>( &'b self, - request: &PaymentsRejectRequest, + request: &PaymentsCancelRequest, merchant_account: &'a domain::MerchantAccount, ) -> RouterResult<( - BoxedOperation<'b, F, PaymentsRejectRequest, Ctx>, + BoxedOperation<'b, F, PaymentsCancelRequest, Ctx>, operations::ValidateResult<'a>, )> { Ok(( diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index b55b0c46f6ad..e5552f0d156d 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; use async_trait::async_trait; +use common_enums::AuthorizationStatus; use data_models::payments::payment_attempt::PaymentAttempt; -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use futures::FutureExt; use router_derive; use router_env::{instrument, tracing}; @@ -11,22 +12,20 @@ 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, }, routes::{metrics, AppState}, services::RedirectForm, types::{ self, api, - storage::{ - self, enums, - payment_attempt::{AttemptStatusExt, PaymentAttemptExt}, - }, - transformers::ForeignTryFrom, + storage::{self, enums}, + transformers::{ForeignFrom, ForeignTryFrom}, CaptureSyncResponse, }, utils, @@ -34,8 +33,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,incremental_authorization_data" )] pub struct PaymentResponse; @@ -75,6 +74,138 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData } } +#[async_trait] +impl PostUpdateTracker, types::PaymentsIncrementalAuthorizationData> + for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + db: &'b AppState, + _payment_id: &api::PaymentIdType, + mut payment_data: PaymentData, + router_data: types::RouterData< + F, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + >, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send, + { + let incremental_authorization_details = payment_data + .incremental_authorization_details + .clone() + .ok_or_else(|| { + report!(errors::ApiErrorResponse::InternalServerError) + .attach_printable("missing incremental_authorization_details in payment_data") + })?; + // Update payment_intent and payment_attempt 'amount' if incremental_authorization is successful + let (option_payment_attempt_update, option_payment_intent_update) = + match router_data.response.clone() { + Err(_) => (None, None), + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { + status, + .. + }) => { + if status == AuthorizationStatus::Success { + (Some( + storage::PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount: incremental_authorization_details.total_amount, + amount_capturable: incremental_authorization_details.total_amount, + }, + ), Some( + storage::PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { + amount: incremental_authorization_details.total_amount, + }, + )) + } else { + (None, None) + } + } + _ => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("unexpected response in incremental_authorization flow")?, + }; + //payment_attempt update + if let Some(payment_attempt_update) = option_payment_attempt_update { + payment_data.payment_attempt = db + .store + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + payment_attempt_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + // payment_intent update + if let Some(payment_intent_update) = option_payment_intent_update { + payment_data.payment_intent = db + .store + .update_payment_intent( + payment_data.payment_intent.clone(), + payment_intent_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + // Update the status of authorization record + let authorization_update = match &router_data.response { + Err(res) => Ok(storage::AuthorizationUpdate::StatusUpdate { + status: AuthorizationStatus::Failure, + error_code: Some(res.code.clone()), + error_message: Some(res.message.clone()), + connector_authorization_id: None, + }), + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { + status, + error_code, + error_message, + connector_authorization_id, + }) => Ok(storage::AuthorizationUpdate::StatusUpdate { + status: status.clone(), + error_code: error_code.clone(), + error_message: error_message.clone(), + connector_authorization_id: connector_authorization_id.clone(), + }), + Ok(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("unexpected response in incremental_authorization flow"), + }?; + let authorization_id = incremental_authorization_details + .authorization_id + .clone() + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing authorization_id in incremental_authorization_details in payment_data", + ), + )?; + db.store + .update_authorization_by_merchant_id_authorization_id( + router_data.merchant_id.clone(), + authorization_id, + authorization_update, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed while updating authorization")?; + //Fetch all the authorizations of the payment and send in incremental authorization response + let authorizations = db + .store + .find_all_authorizations_by_merchant_id_payment_id( + &router_data.merchant_id, + &payment_data.payment_intent.payment_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed while retrieving authorizations")?; + payment_data.authorizations = authorizations; + Ok(payment_data) + } +} + #[async_trait] impl PostUpdateTracker, types::PaymentsSyncData> for PaymentResponse { async fn update_tracker<'b>( @@ -330,21 +461,36 @@ 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 status = + 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 = match err.attempt_status { + // Use the status sent by connector in error_response if it's present + Some(status) => status, + None => // mark previous attempt status for technical failures in PSync flow - if flow_name == "PSync" { - match err.status_code { - // marking failure for 2xx because this is genuine payment failure - 200..=299 => storage::enums::AttemptStatus::Failure, - _ => router_data.status, - } - } else { - match err.status_code { - 500..=511 => storage::enums::AttemptStatus::Pending, - _ => storage::enums::AttemptStatus::Failure, + { + if flow_name == "PSync" { + match err.status_code { + // marking failure for 2xx because this is genuine payment failure + 200..=299 => storage::enums::AttemptStatus::Failure, + _ => router_data.status, + } + } else { + match err.status_code { + 500..=511 => storage::enums::AttemptStatus::Pending, + _ => storage::enums::AttemptStatus::Failure, + } } - }; + } + }; ( None, Some(storage::PaymentAttemptUpdate::ErrorUpdate { @@ -353,94 +499,123 @@ async fn payment_response_update_tracker( error_message: Some(Some(err.message)), error_code: Some(Some(err.code)), error_reason: Some(err.reason), - amount_capturable: if status.is_terminal_status() - || router_data - .status - .maps_to_intent_status(enums::IntentStatus::Processing) - { - Some(0) - } else { - None - }, + amount_capturable: router_data + .request + .get_amount_capturable(&payment_data, status), 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, }), ) } }; (capture_update, attempt_update) } - Ok(payments_response) => match payments_response { - types::PaymentsResponseData::PreProcessingResponse { - pre_processing_id, - connector_metadata, - connector_response_reference_id, - .. - } => { - let connector_transaction_id = match pre_processing_id.to_owned() { - types::PreprocessingResponseId::PreProcessingId(_) => None, - types::PreprocessingResponseId::ConnectorTransactionId(connector_txn_id) => { - Some(connector_txn_id) - } - }; - let preprocessing_step_id = match pre_processing_id { - types::PreprocessingResponseId::PreProcessingId(pre_processing_id) => { - Some(pre_processing_id) + Ok(payments_response) => { + let attempt_status = payment_data.payment_attempt.status.to_owned(); + let connector_status = router_data.status.to_owned(); + let updated_attempt_status = match ( + connector_status, + attempt_status, + payment_data.frm_message.to_owned(), + ) { + ( + enums::AttemptStatus::Authorized, + enums::AttemptStatus::Unresolved, + Some(frm_message), + ) => match frm_message.frm_status { + enums::FraudCheckStatus::Fraud | enums::FraudCheckStatus::ManualReview => { + attempt_status } - types::PreprocessingResponseId::ConnectorTransactionId(_) => None, - }; - let payment_attempt_update = storage::PaymentAttemptUpdate::PreprocessingUpdate { - status: router_data.status, - payment_method_id: Some(router_data.payment_method_id), + _ => router_data.get_attempt_status_for_db_update(&payment_data), + }, + _ => router_data.get_attempt_status_for_db_update(&payment_data), + }; + match payments_response { + types::PaymentsResponseData::PreProcessingResponse { + pre_processing_id, connector_metadata, - preprocessing_step_id, - connector_transaction_id, connector_response_reference_id, - updated_by: storage_scheme.to_string(), - }; - - (None, Some(payment_attempt_update)) - } - types::PaymentsResponseData::TransactionResponse { - resource_id, - redirection_data, - connector_metadata, - connector_response_reference_id, - .. - } => { - let connector_transaction_id = match resource_id { - types::ResponseId::NoResponseId => None, - types::ResponseId::ConnectorTransactionId(id) - | types::ResponseId::EncodedData(id) => Some(id), - }; - - let encoded_data = payment_data.payment_attempt.encoded_data.clone(); - - let authentication_data = redirection_data - .map(|data| utils::Encode::::encode_to_value(&data)) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not parse the connector response")?; - - // incase of success, update error code and error message - let error_status = if router_data.status == enums::AttemptStatus::Charged { - Some(None) - } else { - None - }; + .. + } => { + let connector_transaction_id = match pre_processing_id.to_owned() { + types::PreprocessingResponseId::PreProcessingId(_) => None, + types::PreprocessingResponseId::ConnectorTransactionId( + connector_txn_id, + ) => Some(connector_txn_id), + }; + let preprocessing_step_id = match pre_processing_id { + types::PreprocessingResponseId::PreProcessingId(pre_processing_id) => { + Some(pre_processing_id) + } + types::PreprocessingResponseId::ConnectorTransactionId(_) => None, + }; + let payment_attempt_update = + storage::PaymentAttemptUpdate::PreprocessingUpdate { + status: updated_attempt_status, + payment_method_id: Some(router_data.payment_method_id), + connector_metadata, + preprocessing_step_id, + connector_transaction_id, + connector_response_reference_id, + updated_by: storage_scheme.to_string(), + }; - if router_data.status == enums::AttemptStatus::Charged { - metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]); + (None, Some(payment_attempt_update)) } + types::PaymentsResponseData::TransactionResponse { + resource_id, + 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) + | types::ResponseId::EncodedData(id) => Some(id), + }; + + let encoded_data = payment_data.payment_attempt.encoded_data.clone(); + + let authentication_data = redirection_data + .map(|data| utils::Encode::::encode_to_value(&data)) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not parse the connector response")?; + + // incase of success, update error code and error message + let error_status = if router_data.status == enums::AttemptStatus::Charged { + Some(None) + } else { + None + }; + + if router_data.status == enums::AttemptStatus::Charged { + metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]); + } - utils::add_apple_pay_payment_status_metrics( - router_data.status, - router_data.apple_pay_flow, - payment_data.payment_attempt.connector.clone(), - payment_data.payment_attempt.merchant_id.clone(), - ); + utils::add_apple_pay_payment_status_metrics( + router_data.status, + router_data.apple_pay_flow.clone(), + payment_data.payment_attempt.connector.clone(), + payment_data.payment_attempt.merchant_id.clone(), + ); - let (capture_updates, payment_attempt_update) = - match payment_data.multiple_capture_data { + let (capture_updates, payment_attempt_update) = match payment_data + .multiple_capture_data + { Some(multiple_capture_data) => { let capture_update = storage::CaptureUpdate::ResponseUpdate { status: enums::CaptureStatus::foreign_try_from(router_data.status)?, @@ -456,10 +631,13 @@ async fn payment_response_update_tracker( None => ( None, Some(storage::PaymentAttemptUpdate::ResponseUpdate { - status: router_data.status, + status: updated_attempt_status, connector: None, connector_transaction_id: connector_transaction_id.clone(), authentication_type: None, + amount_capturable: router_data + .request + .get_amount_capturable(&payment_data, updated_attempt_status), payment_method_id: Some(router_data.payment_method_id), mandate_id: payment_data .mandate_id @@ -469,19 +647,10 @@ 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 - .status - .maps_to_intent_status(enums::IntentStatus::Processing) - { - Some(0) - } 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, @@ -489,51 +658,55 @@ async fn payment_response_update_tracker( ), }; - (capture_updates, payment_attempt_update) - } - types::PaymentsResponseData::TransactionUnresolvedResponse { - resource_id, - reason, - connector_response_reference_id, - } => { - let connector_transaction_id = match resource_id { - types::ResponseId::NoResponseId => None, - types::ResponseId::ConnectorTransactionId(id) - | types::ResponseId::EncodedData(id) => Some(id), - }; - ( - None, - Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate { - status: router_data.status, - connector: None, - connector_transaction_id, - payment_method_id: Some(router_data.payment_method_id), - error_code: Some(reason.clone().map(|cd| cd.code)), - error_message: Some(reason.clone().map(|cd| cd.message)), - error_reason: Some(reason.map(|cd| cd.message)), - connector_response_reference_id, - updated_by: storage_scheme.to_string(), - }), - ) - } - types::PaymentsResponseData::SessionResponse { .. } => (None, None), - types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), - types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), - types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), - types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), - types::PaymentsResponseData::MultipleCaptureResponse { - capture_sync_response_list, - } => match payment_data.multiple_capture_data { - Some(multiple_capture_data) => { - let capture_update_list = response_to_capture_update( - &multiple_capture_data, - capture_sync_response_list, - )?; - (Some((multiple_capture_data, capture_update_list)), None) + (capture_updates, payment_attempt_update) } - None => (None, None), - }, - }, + types::PaymentsResponseData::TransactionUnresolvedResponse { + resource_id, + reason, + connector_response_reference_id, + } => { + let connector_transaction_id = match resource_id { + types::ResponseId::NoResponseId => None, + types::ResponseId::ConnectorTransactionId(id) + | types::ResponseId::EncodedData(id) => Some(id), + }; + ( + None, + Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate { + status: updated_attempt_status, + connector: None, + connector_transaction_id, + payment_method_id: Some(router_data.payment_method_id), + error_code: Some(reason.clone().map(|cd| cd.code)), + error_message: Some(reason.clone().map(|cd| cd.message)), + error_reason: Some(reason.map(|cd| cd.message)), + connector_response_reference_id, + updated_by: storage_scheme.to_string(), + }), + ) + } + types::PaymentsResponseData::SessionResponse { .. } => (None, None), + types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), + types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), + types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), + types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), + types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => { + (None, None) + } + types::PaymentsResponseData::MultipleCaptureResponse { + capture_sync_response_list, + } => match payment_data.multiple_capture_data { + Some(multiple_capture_data) => { + let capture_update_list = response_to_capture_update( + &multiple_capture_data, + capture_sync_response_list, + )?; + (Some((multiple_capture_data, capture_update_list)), None) + } + None => (None, None), + }, + } + } }; payment_data.multiple_capture_data = match capture_update { Some((mut multiple_capture_data, capture_updates)) => { @@ -610,18 +783,23 @@ async fn payment_response_update_tracker( 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(), + // make this false only if initial payment fails, if incremental authorization call fails don't make it false + 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, }, }; @@ -735,7 +913,7 @@ fn get_total_amount_captured( } None => { //Non multiple capture - let amount = request.get_capture_amount(); + let amount = request.get_captured_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 3abde60c2e9b..170db39388b7 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,8 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsSessionRequest, Ctx>, - PaymentData, - Option, - )> { + _payment_confirm_source: Option, + ) -> RouterResult> { let payment_id = payment_id .get_payment_intent_id() .change_context(errors::ApiErrorResponse::PaymentNotFound)?; @@ -70,18 +67,7 @@ impl "create a session token for", )?; - let intent_fulfillment_time = helpers::get_merchant_fullfillment_time( - payment_intent.payment_link_id.clone(), - merchant_account.intent_fulfillment_time, - db, - ) - .await?; - - helpers::authenticate_client_secret( - Some(&request.client_secret), - &payment_intent, - intent_fulfillment_time, - )?; + helpers::authenticate_client_secret(Some(&request.client_secret), &payment_intent)?; let mut payment_attempt = db .find_payment_attempt_by_payment_id_merchant_id_attempt_id( @@ -97,7 +83,7 @@ impl payment_attempt.payment_method = Some(storage_enums::PaymentMethod::Wallet); - let amount = payment_intent.amount.into(); + let amount = payment_attempt.get_total_amount().into(); let shipping_address = helpers::create_or_find_address_for_payment_by_request( db, @@ -152,44 +138,66 @@ 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, + incremental_authorization_details: None, + authorizations: vec![], + frm_metadata: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -305,6 +313,7 @@ where _payment_data: &mut PaymentData, _storage_scheme: storage_enums::MerchantStorageScheme, _merchant_key_store: &domain::MerchantKeyStore, + _customer: &Option, ) -> RouterResult<( BoxedOperation<'b, F, api::PaymentsSessionRequest, Ctx>, Option, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 17f39d5150bb..8b4ec4e3f546 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,8 @@ impl merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsStartRequest, Ctx>, - PaymentData, - Option, - )> { + _payment_confirm_source: Option, + ) -> RouterResult> { let (mut payment_intent, payment_attempt, currency, amount); let db = &*state.store; @@ -70,17 +67,9 @@ impl "update", )?; - let intent_fulfillment_time = helpers::get_merchant_fullfillment_time( - payment_intent.payment_link_id.clone(), - merchant_account.intent_fulfillment_time, - db, - ) - .await?; - helpers::authenticate_client_secret( payment_intent.client_secret.as_ref(), &payment_intent, - intent_fulfillment_time, )?; payment_attempt = db .find_payment_attempt_by_payment_id_merchant_id_attempt_id( @@ -93,7 +82,7 @@ impl .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); + amount = payment_attempt.get_total_amount().into(); let shipping_address = helpers::create_or_find_address_for_payment_by_request( db, @@ -126,44 +115,66 @@ 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, + incremental_authorization_details: None, + authorizations: vec![], + frm_metadata: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -268,6 +279,7 @@ where payment_data: &mut PaymentData, _storage_scheme: storage_enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsStartRequest, Ctx>, Option, @@ -279,7 +291,14 @@ where .map(|connector_name| connector_name == *"bluesnap".to_string()) .unwrap_or(false) { - helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await + helpers::make_pm_data( + Box::new(self), + state, + payment_data, + merchant_key_store, + customer, + ) + .await } else { Ok((Box::new(self), None)) } diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index fb58aeb34e07..9db9742ca9ea 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 @@ -96,11 +96,19 @@ impl Domain, _storage_scheme: enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await + helpers::make_pm_data( + Box::new(self), + state, + payment_data, + merchant_key_store, + customer, + ) + .await } #[instrument(skip_all)] @@ -190,11 +198,9 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, - PaymentData, - Option, - )> { + _payment_confirm_source: Option, + ) -> RouterResult> + { get_tracker_for_sync( payment_id, merchant_account, @@ -221,12 +227,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, @@ -236,23 +238,12 @@ async fn get_tracker_for_sync< ) .await?; - let intent_fulfillment_time = helpers::get_merchant_fullfillment_time( - payment_intent.payment_link_id.clone(), - merchant_account.intent_fulfillment_time, - db, - ) - .await?; - helpers::authenticate_client_secret( - request.client_secret.as_ref(), - &payment_intent, - intent_fulfillment_time, - )?; + helpers::authenticate_client_secret(request.client_secret.as_ref(), &payment_intent)?; 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(); + amount = payment_attempt.get_total_amount().into(); let shipping_address = helpers::get_address_by_id( db, @@ -322,6 +313,20 @@ async fn get_tracker_for_sync< ) })?; + let authorizations = db + .find_all_authorizations_by_merchant_id_payment_id( + &merchant_account.merchant_id, + &payment_id_str, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable_lazy(|| { + format!( + "Failed while getting authorizations list for, payment_id: {}, merchant_id: {}", + &payment_id_str, merchant_account.merchant_id + ) + })?; + let disputes = db .find_disputes_by_merchant_id_payment_id(&merchant_account.merchant_id, &payment_id_str) .await @@ -357,53 +362,77 @@ 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(), + incremental_authorization_details: None, + authorizations, + frm_metadata: None, + }; + + 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 53a768f26810..664fce820a0e 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -1,9 +1,9 @@ use std::marker::PhantomData; -use api_models::enums::FrmSuggestion; +use api_models::{enums::FrmSuggestion, payments::RequestSurchargeDetails}; 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}; @@ -21,13 +21,13 @@ use crate::{ types::{ api::{self, PaymentIdTypeExt}, domain, - storage::{self, enums as storage_enums}, + storage::{self, enums as storage_enums, payment_attempt::PaymentAttemptExt}, }, utils::OptionExt, }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentUpdate; #[async_trait] @@ -44,11 +44,8 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + _payment_confirm_source: Option, + ) -> RouterResult> { let (mut payment_intent, mut payment_attempt, currency): (_, _, storage_enums::Currency); let payment_id = payment_id @@ -64,6 +61,14 @@ 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, + false, + )?; + } + payment_intent.setup_future_usage = request .setup_future_usage .or(payment_intent.setup_future_usage); @@ -77,23 +82,13 @@ impl &[ storage_enums::IntentStatus::Failed, storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::PartiallyCaptured, storage_enums::IntentStatus::RequiresCapture, ], "update", )?; - let intent_fulfillment_time = helpers::get_merchant_fullfillment_time( - payment_intent.payment_link_id.clone(), - merchant_account.intent_fulfillment_time, - db, - ) - .await?; - - helpers::authenticate_client_secret( - request.client_secret.as_ref(), - &payment_intent, - intent_fulfillment_time, - )?; + helpers::authenticate_client_secret(request.client_secret.as_ref(), &payment_intent)?; let ( token, payment_method, @@ -131,6 +126,20 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + helpers::validate_amount_to_capture_and_capture_method(Some(&payment_attempt), request)?; + + helpers::validate_request_amount_and_amount_to_capture( + request.amount, + request.amount_to_capture, + request + .surcharge_details + .or(payment_attempt.get_surcharge_details()), + ) + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "amount_to_capture".to_string(), + expected_format: "amount_to_capture lesser than or equal to amount".to_string(), + })?; + currency = request .currency .or(payment_attempt.currency) @@ -247,10 +256,7 @@ impl api_models::payments::MandateIds { mandate_id: mandate_obj.mandate_id, mandate_reference_id: Some(api_models::payments::MandateReferenceId::ConnectorMandateId( - api_models::payments::ConnectorMandateReferenceId { - connector_mandate_id: connector_id.connector_mandate_id, - payment_method_id: connector_id.payment_method_id, - }, + api_models::payments::ConnectorMandateReferenceId {connector_mandate_id:connector_id.connector_mandate_id,payment_method_id:connector_id.payment_method_id, update_history: None }, )) } }), @@ -263,11 +269,25 @@ impl }) .await .transpose()?; - let next_operation: BoxedOperation<'a, F, api::PaymentsRequest, Ctx> = + let (next_operation, amount): (BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, _) = if request.confirm.unwrap_or(false) { - Box::new(operations::PaymentConfirm) + let amount = { + let amount = request + .amount + .map(Into::into) + .unwrap_or(payment_attempt.amount); + payment_attempt.amount = amount; + payment_intent.amount = amount; + let surcharge_amount = request + .surcharge_details + .as_ref() + .map(RequestSurchargeDetails::get_total_surcharge_amount) + .or(payment_attempt.get_total_surcharge_amount()); + (amount + surcharge_amount.unwrap_or(0)).into() + }; + (Box::new(operations::PaymentConfirm), amount) } else { - Box::new(self) + (Box::new(self), amount) }; payment_intent.status = match request.payment_method_data.as_ref() { @@ -304,48 +324,70 @@ 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) + payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); - 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, + incremental_authorization_details: None, + authorizations: vec![], + frm_metadata: request.frm_metadata.clone(), + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: next_operation, + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } @@ -385,11 +427,19 @@ impl Domain, _storage_scheme: storage_enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, + customer: &Option, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, Option, )> { - helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await + helpers::make_pm_data( + Box::new(self), + state, + payment_data, + merchant_key_store, + customer, + ) + .await } #[instrument(skip_all)] @@ -541,11 +591,12 @@ impl .clone(); let order_details = payment_data.payment_intent.order_details.clone(); let metadata = payment_data.payment_intent.metadata.clone(); + let session_expiry = payment_data.payment_intent.session_expiry; payment_data.payment_intent = state .store .update_payment_intent( - payment_data.payment_intent, + payment_data.payment_intent.clone(), storage::PaymentIntentUpdate::Update { amount: payment_data.amount.into(), currency: payment_data.currency, @@ -564,6 +615,8 @@ impl metadata, payment_confirm_source: None, updated_by: storage_scheme.to_string(), + fingerprint_id: None, + session_expiry, }, storage_scheme, ) @@ -592,14 +645,13 @@ 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, - }; + if let Some(session_expiry) = &request.session_expiry { + helpers::validate_session_expiry(session_expiry.to_owned())?; + } + 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) @@ -611,6 +663,7 @@ impl ValidateRequest ValidateRequest + GetTracker, PaymentsIncrementalAuthorizationRequest, Ctx> + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_id: &api::PaymentIdType, + request: &PaymentsIncrementalAuthorizationRequest, + _mandate_type: Option, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, + ) -> RouterResult< + operations::GetTrackerResponse<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + > { + let db = &*state.store; + let merchant_id = &merchant_account.merchant_id; + let storage_scheme = merchant_account.storage_scheme; + let payment_id = payment_id + .get_payment_intent_id() + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + helpers::validate_payment_status_against_allowed_statuses( + &payment_intent.status, + &[enums::IntentStatus::RequiresCapture], + "increment authorization", + )?; + + if payment_intent.incremental_authorization_allowed != Some(true) { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: + "You cannot increment authorization this payment because it is not allowed for incremental_authorization".to_owned(), + })? + } + + if request.amount < payment_intent.amount { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Amount should be greater than original authorized amount".to_owned(), + })? + } + + let attempt_id = payment_intent.active_attempt.get_id().clone(); + let payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + payment_intent.payment_id.as_str(), + merchant_id, + attempt_id.clone().as_str(), + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let currency = payment_attempt.currency.get_required_value("currency")?; + let amount = payment_attempt.get_total_amount(); + + 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 = payments::PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount: amount.into(), + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + billing: None, + shipping: 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: None, + payment_link_data: None, + incremental_authorization_details: Some(IncrementalAuthorizationDetails { + additional_amount: request.amount - amount, + total_amount: request.amount, + reason: request.reason.clone(), + authorization_id: None, + }), + authorizations: vec![], + frm_metadata: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl + UpdateTracker, PaymentsIncrementalAuthorizationRequest, Ctx> + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + db: &'b AppState, + mut payment_data: payments::PaymentData, + _customer: Option, + storage_scheme: enums::MerchantStorageScheme, + _updated_customer: Option, + _mechant_key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: api::HeaderPayload, + ) -> RouterResult<( + BoxedOperation<'b, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + payments::PaymentData, + )> + where + F: 'b + Send, + { + let new_authorization_count = payment_data + .payment_intent + .authorization_count + .map(|count| count + 1) + .unwrap_or(1); + // Create new authorization record + let authorization_new = AuthorizationNew { + authorization_id: format!( + "{}_{}", + common_utils::generate_id_with_default_len("auth"), + new_authorization_count + ), + merchant_id: payment_data.payment_intent.merchant_id.clone(), + payment_id: payment_data.payment_intent.payment_id.clone(), + amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.total_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + status: common_enums::AuthorizationStatus::Processing, + error_code: None, + error_message: None, + connector_authorization_id: None, + previously_authorized_amount: payment_data.payment_intent.amount, + }; + let authorization = db + .store + .insert_authorization(authorization_new.clone()) + .await + .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { + message: format!( + "Authorization with authorization_id {} already exists", + authorization_new.authorization_id + ), + }) + .attach_printable("failed while inserting new authorization")?; + // Update authorization_count in payment_intent + payment_data.payment_intent = db + .store + .update_payment_intent( + payment_data.payment_intent.clone(), + storage::PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count: new_authorization_count, + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update authorization_count in Payment Intent")?; + match &payment_data.incremental_authorization_details { + Some(details) => { + payment_data.incremental_authorization_details = + Some(IncrementalAuthorizationDetails { + authorization_id: Some(authorization.authorization_id), + ..details.clone() + }); + } + None => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("missing incremental_authorization_details in payment_data")?, + } + Ok((Box::new(self), payment_data)) + } +} + +impl + ValidateRequest + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + request: &PaymentsIncrementalAuthorizationRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult<( + BoxedOperation<'b, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + operations::ValidateResult<'a>, + )> { + Ok(( + Box::new(self), + operations::ValidateResult { + merchant_id: &merchant_account.merchant_id, + payment_id: api::PaymentIdType::PaymentIntentId(request.payment_id.to_owned()), + mandate_type: None, + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }, + )) + } +} + +#[async_trait] +impl + Domain for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn get_or_create_customer_details<'a>( + &'a self, + _db: &dyn StorageInterface, + _payment_data: &mut payments::PaymentData, + _request: Option, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult< + ( + BoxedOperation<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + Option, + ), + errors::StorageError, + > { + Ok((Box::new(self), None)) + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a AppState, + _payment_data: &mut payments::PaymentData, + _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, + _customer: &Option, + ) -> RouterResult<( + BoxedOperation<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + Option, + )> { + Ok((Box::new(self), None)) + } + + async fn get_connector<'a>( + &'a self, + _merchant_account: &domain::MerchantAccount, + state: &AppState, + _request: &PaymentsIncrementalAuthorizationRequest, + _payment_intent: &storage::PaymentIntent, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + helpers::get_connector_default(state, None).await + } +} diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 376b9048c856..8d74eb3fa961 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -40,6 +40,7 @@ pub async fn do_gsm_actions( customer: &Option, validate_result: &operations::ValidateResult<'_>, schedule_time: Option, + frm_suggestion: Option, ) -> RouterResult> where F: Clone + Send + Sync, @@ -55,7 +56,7 @@ where metrics::AUTO_RETRY_ELIGIBLE_REQUEST_COUNT.add(&metrics::CONTEXT, 1, &[]); - let mut initial_gsm = get_gsm(state, &router_data).await; + let mut initial_gsm = get_gsm(state, &router_data).await?; //Check if step-up to threeDS is possible and merchant has enabled let step_up_possible = initial_gsm @@ -90,6 +91,7 @@ where validate_result, schedule_time, true, + frm_suggestion, ) .await?; } @@ -99,7 +101,7 @@ where // Use initial_gsm for first time alone let gsm = match initial_gsm.as_ref() { Some(gsm) => Some(gsm.clone()), - None => get_gsm(state, &router_data).await, + None => get_gsm(state, &router_data).await?, }; match get_gsm_decision(gsm) { @@ -133,6 +135,7 @@ where schedule_time, //this is an auto retry payment, but not step-up false, + frm_suggestion, ) .await?; @@ -214,46 +217,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)] @@ -305,6 +278,7 @@ pub async fn do_retry( validate_result: &operations::ValidateResult<'_>, schedule_time: Option, is_step_up: bool, + frm_suggestion: Option, ) -> RouterResult> where F: Clone + Send + Sync, @@ -340,6 +314,7 @@ where validate_result, schedule_time, api::HeaderPayload::default(), + frm_suggestion, ) .await } @@ -412,11 +387,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 +402,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 +545,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 5704f82f4983..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,72 +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::Bambora => Self::Bambora, - api_enums::RoutableConnectors::Bankofamerica => Self::Bankofamerica, - api_enums::RoutableConnectors::Bitpay => Self::Bitpay, - 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::Prophetpay => Self::Prophetpay, - 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..f52bce46d8ee 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -14,7 +14,7 @@ use crate::{ services, types::{ self, - api::{self, CardDetailsPaymentMethod, PaymentMethodCreateExt}, + api::{self, CardDetailFromLocker, CardDetailsPaymentMethod, PaymentMethodCreateExt}, domain, storage::enums as storage_enums, }, @@ -74,12 +74,21 @@ where .await?; let merchant_id = &merchant_account.merchant_id; - let locker_response = save_in_locker( - state, - merchant_account, - payment_method_create_request.to_owned(), - ) - .await?; + let locker_response = if !state.conf.locker.locker_enabled { + skip_saving_card_in_locker( + merchant_account, + payment_method_create_request.to_owned(), + ) + .await? + } else { + Box::pin(save_in_locker( + state, + merchant_account, + payment_method_create_request.to_owned(), + )) + .await? + }; + let is_duplicate = locker_response.1; let pm_card_details = locker_response.0.card.as_ref().map(|card| { @@ -168,6 +177,85 @@ where } } +async fn skip_saving_card_in_locker( + merchant_account: &domain::MerchantAccount, + payment_method_request: api::PaymentMethodCreate, +) -> RouterResult<(api_models::payment_methods::PaymentMethodResponse, bool)> { + let merchant_id = &merchant_account.merchant_id; + let customer_id = payment_method_request + .clone() + .customer_id + .clone() + .get_required_value("customer_id")?; + let payment_method_id = common_utils::generate_id(crate::consts::ID_LENGTH, "pm"); + + let last4_digits = payment_method_request + .card + .clone() + .map(|c| c.card_number.get_last4()); + + let card_isin = payment_method_request + .card + .clone() + .map(|c: api_models::payment_methods::CardDetail| c.card_number.get_card_isin()); + + match payment_method_request.card.clone() { + Some(card) => { + let card_detail = CardDetailFromLocker { + scheme: None, + issuer_country: card.card_issuing_country.clone(), + last4_digits: last4_digits.clone(), + card_number: None, + expiry_month: Some(card.card_exp_month.clone()), + expiry_year: Some(card.card_exp_year), + card_token: None, + card_holder_name: card.card_holder_name.clone(), + card_fingerprint: None, + nick_name: None, + card_isin: card_isin.clone(), + card_issuer: card.card_issuer.clone(), + card_network: card.card_network.clone(), + card_type: card.card_type.clone(), + saved_to_locker: false, + }; + let pm_resp = api::PaymentMethodResponse { + merchant_id: merchant_id.to_string(), + customer_id: Some(customer_id), + payment_method_id, + payment_method: payment_method_request.payment_method, + payment_method_type: payment_method_request.payment_method_type, + card: Some(card_detail), + recurring_enabled: false, + installment_payment_enabled: false, + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + metadata: None, + created: Some(common_utils::date_time::now()), + bank_transfer: None, + }; + + Ok((pm_resp, false)) + } + None => { + let pm_id = common_utils::generate_id(crate::consts::ID_LENGTH, "pm"); + let payment_method_response = api::PaymentMethodResponse { + merchant_id: merchant_id.to_string(), + customer_id: Some(customer_id), + payment_method_id: pm_id, + payment_method: payment_method_request.payment_method, + payment_method_type: payment_method_request.payment_method_type, + card: None, + metadata: None, + created: Some(common_utils::date_time::now()), + recurring_enabled: false, + installment_payment_enabled: false, + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + bank_transfer: None, + }; + Ok((payment_method_response, false)) + } + } +} + pub async fn save_in_locker( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -183,8 +271,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 @@ -198,6 +286,7 @@ pub async fn save_in_locker( payment_method_id: pm_id, payment_method: payment_method_request.payment_method, payment_method_type: payment_method_request.payment_method_type, + bank_transfer: None, card: None, metadata: None, created: Some(common_utils::date_time::now()), diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 6c6b4ae9339f..1b4b2fc9fc35 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1,9 +1,10 @@ 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}; +use error_stack::{report, IntoReport, ResultExt}; use router_env::{instrument, tracing}; use super::{flows::Feature, PaymentData}; @@ -33,7 +34,7 @@ pub async fn construct_payment_router_data<'a, F, T>( connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, - customer: &Option, + customer: &'a Option, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult> where @@ -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 { @@ -87,6 +89,7 @@ where connector_name: connector_id.to_string(), payment_data: payment_data.clone(), state, + customer_data: customer, }; let customer_id = customer.to_owned().map(|customer| customer.customer_id); @@ -116,7 +119,7 @@ where let apple_pay_flow = payments::decide_apple_pay_flow( &payment_data.payment_attempt.payment_method_type, - &Some(merchant_connector_account.clone()), + Some(merchant_connector_account), ); router_data = types::RouterData { @@ -163,6 +166,9 @@ where connector_http_status_code: None, external_latency: None, apple_pay_flow, + frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) @@ -390,6 +396,18 @@ where ) }; + let incremental_authorizations_response = if payment_data.authorizations.is_empty() { + None + } else { + Some( + payment_data + .authorizations + .into_iter() + .map(ForeignInto::foreign_into) + .collect(), + ) + }; + let attempts_response = payment_data.attempts.map(|attempts| { attempts .into_iter() @@ -506,10 +524,7 @@ where } })) .or(next_action_containing_qr_code_url.map(|qr_code_data| { - api_models::payments::NextActionData::QrCodeInformation { - image_data_url: qr_code_data.image_data_url, - display_to_timestamp: qr_code_data.display_to_timestamp, - } + api_models::payments::NextActionData::foreign_from(qr_code_data) })) .or(next_action_containing_wait_screen.map(|wait_screen_data| { api_models::payments::NextActionData::WaitScreenInformation { @@ -532,7 +547,7 @@ where if third_party_sdk_session_next_action(&payment_attempt, operation) { next_action_response = Some( api_models::payments::NextActionData::ThirdPartySdkSessionToken { - session_token: payment_data.sessions_token.get(0).cloned(), + session_token: payment_data.sessions_token.first().cloned(), }, ) } @@ -550,6 +565,7 @@ where }); services::ApplicationResponse::JsonWithHeaders(( response + .set_net_amount(payment_attempt.net_amount) .set_payment_id(Some(payment_attempt.payment_id)) .set_merchant_id(Some(payment_attempt.merchant_id)) .set_status(payment_intent.status) @@ -622,6 +638,7 @@ where api::MandateType::MultiUse(None) } }), + update_mandate_id: d.update_mandate_id, }), auth_flow == services::AuthFlow::Merchant, ) @@ -685,6 +702,15 @@ 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, + ) + .set_fingerprint(payment_intent.fingerprint_id) + .set_authorization_count(payment_intent.authorization_count) + .set_incremental_authorizations(incremental_authorizations_response) + .set_expires_on(payment_intent.session_expiry) .to_owned(), headers, )) @@ -692,6 +718,7 @@ where } None => services::ApplicationResponse::JsonWithHeaders(( api::PaymentsResponse { + net_amount: payment_attempt.net_amount, payment_id: Some(payment_attempt.payment_id), merchant_id: Some(payment_attempt.merchant_id), status: payment_intent.status, @@ -745,6 +772,12 @@ 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, + authorization_count: payment_intent.authorization_count, + incremental_authorizations: incremental_authorizations_response, + expires_on: payment_intent.session_expiry, ..Default::default() }, headers, @@ -797,11 +830,10 @@ where pub fn qr_code_next_steps_check( payment_attempt: storage::PaymentAttempt, -) -> RouterResult> { - let qr_code_steps: Option> = - payment_attempt - .connector_metadata - .map(|metadata| metadata.parse_value("QrCodeNextStepsInstruction")); +) -> RouterResult> { + let qr_code_steps: Option> = payment_attempt + .connector_metadata + .map(|metadata| metadata.parse_value("QrCodeInformation")); let qr_code_instructions = qr_code_steps.transpose().ok().flatten(); Ok(qr_code_instructions) @@ -871,17 +903,24 @@ pub fn bank_transfer_next_steps_check( let bank_transfer_next_step = if let Some(diesel_models::enums::PaymentMethod::BankTransfer) = payment_attempt.payment_method { - let bank_transfer_next_steps: Option = - payment_attempt - .connector_metadata - .map(|metadata| { - metadata - .parse_value("NextStepsRequirements") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to parse the Value to NextRequirements struct") - }) - .transpose()?; - bank_transfer_next_steps + if payment_attempt.payment_method_type != Some(diesel_models::enums::PaymentMethodType::Pix) + { + let bank_transfer_next_steps: Option = + payment_attempt + .connector_metadata + .map(|metadata| { + metadata + .parse_value("NextStepsRequirements") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Failed to parse the Value to NextRequirements struct", + ) + }) + .transpose()?; + bank_transfer_next_steps + } else { + None + } } else { None }; @@ -919,9 +958,46 @@ pub fn change_order_details_to_new_type( quantity: order_details.quantity, amount: order_amount, product_img_link: order_details.product_img_link, + requires_shipping: order_details.requires_shipping, + product_id: order_details.product_id, + category: order_details.category, + brand: order_details.brand, + product_type: order_details.product_type, }]) } +impl ForeignFrom for api_models::payments::NextActionData { + fn foreign_from(qr_info: api_models::payments::QrCodeInformation) -> Self { + match qr_info { + api_models::payments::QrCodeInformation::QrCodeUrl { + image_data_url, + qr_code_url, + display_to_timestamp, + } => Self::QrCodeInformation { + image_data_url: Some(image_data_url), + qr_code_url: Some(qr_code_url), + display_to_timestamp, + }, + api_models::payments::QrCodeInformation::QrDataUrl { + image_data_url, + display_to_timestamp, + } => Self::QrCodeInformation { + image_data_url: Some(image_data_url), + display_to_timestamp, + qr_code_url: None, + }, + api_models::payments::QrCodeInformation::QrCodeImageUrl { + qr_code_url, + display_to_timestamp, + } => Self::QrCodeInformation { + qr_code_url: Some(qr_code_url), + display_to_timestamp, + image_data_url: None, + }, + } + } +} + #[derive(Clone)] pub struct PaymentAdditionalData<'a, F> where @@ -931,6 +1007,7 @@ where connector_name: String, payment_data: PaymentData, state: &'a AppState, + customer_data: &'a Option, } impl TryFrom> for types::PaymentsAuthorizeData { type Error = error_stack::Report; @@ -1006,6 +1083,22 @@ impl TryFrom> for types::PaymentsAuthoriz None } }); + let amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount) + .unwrap_or(payment_data.amount.into()); + + let customer_name = additional_data + .customer_data + .as_ref() + .and_then(|customer_data| { + customer_data + .name + .as_ref() + .map(|customer| customer.clone().into_inner()) + }); + Ok(Self { payment_method_data: payment_method_data.get_required_value("payment_method_data")?, setup_future_usage: payment_data.payment_intent.setup_future_usage, @@ -1016,10 +1109,11 @@ impl TryFrom> for types::PaymentsAuthoriz statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, statement_descriptor: payment_data.payment_intent.statement_descriptor_name, capture_method: payment_data.payment_attempt.capture_method, - amount: payment_data.amount.into(), + amount, currency: payment_data.currency, browser_info, email: payment_data.email, + customer_name, payment_experience: payment_data.payment_attempt.payment_experience, order_details, order_category, @@ -1032,6 +1126,14 @@ 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, + Some(RequestIncrementalAuthorization::True) + | Some(RequestIncrementalAuthorization::Default) + ), + metadata: additional_data.payment_data.payment_intent.metadata, }) } } @@ -1062,6 +1164,50 @@ impl TryFrom> for types::PaymentsSyncData } } +impl TryFrom> + for types::PaymentsIncrementalAuthorizationData +{ + type Error = error_stack::Report; + + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { + let payment_data = additional_data.payment_data; + let connector = api::ConnectorData::get_connector_by_name( + &additional_data.state.conf.connectors, + &additional_data.connector_name, + api::GetToken::Connector, + payment_data.payment_attempt.merchant_connector_id.clone(), + )?; + Ok(Self { + total_amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.total_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + additional_amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.additional_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + reason: payment_data + .incremental_authorization_details + .and_then(|details| details.reason), + currency: payment_data.currency, + connector_transaction_id: connector + .connector + .connector_transaction_id(payment_data.payment_attempt.clone())? + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, + }) + } +} + impl api::ConnectorTransactionId for Helcim { fn connector_transaction_id( &self, @@ -1132,6 +1278,7 @@ impl TryFrom> for types::PaymentsCaptureD None => None, }, browser_info, + metadata: payment_data.payment_intent.metadata, }) } } @@ -1166,6 +1313,7 @@ impl TryFrom> for types::PaymentsCancelDa cancellation_reason: payment_data.payment_attempt.cancellation_reason, connector_meta: payment_data.payment_attempt.connector_metadata, browser_info, + metadata: payment_data.payment_intent.metadata, }) } } @@ -1218,9 +1366,14 @@ impl TryFrom> for types::PaymentsSessionD .collect::, _>>() }) .transpose()?; + let amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount) + .unwrap_or(payment_data.amount.into()); Ok(Self { - amount: payment_data.amount.into(), + amount, currency: payment_data.currency, country: payment_data.address.billing.and_then(|billing_address| { billing_address.address.and_then(|address| address.country) @@ -1253,6 +1406,17 @@ impl TryFrom> for types::SetupMandateRequ .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "browser_info", })?; + + let customer_name = additional_data + .customer_data + .as_ref() + .and_then(|customer_data| { + customer_data + .name + .as_ref() + .map(|customer| customer.clone().into_inner()) + }); + Ok(Self { currency: payment_data.currency, confirm: true, @@ -1267,9 +1431,18 @@ impl TryFrom> for types::SetupMandateRequ setup_mandate_details: payment_data.setup_mandate, router_return_url, email: payment_data.email, + customer_name, 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, + Some(RequestIncrementalAuthorization::True) + | Some(RequestIncrementalAuthorization::Default) + ), + metadata: payment_data.payment_intent.metadata.clone(), }) } } @@ -1319,6 +1492,9 @@ impl TryFrom> for types::CompleteAuthoriz fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { let payment_data = additional_data.payment_data; + let router_base_url = &additional_data.router_base_url; + let connector_name = &additional_data.connector_name; + let attempt = &payment_data.payment_attempt; let browser_info: Option = payment_data .payment_attempt .browser_info @@ -1335,7 +1511,16 @@ impl TryFrom> for types::CompleteAuthoriz payload: redirect.json_payload, } }); - + let amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount) + .unwrap_or(payment_data.amount.into()); + let complete_authorize_url = Some(helpers::create_complete_authorize_url( + router_base_url, + attempt, + connector_name, + )); Ok(Self { setup_future_usage: payment_data.payment_intent.setup_future_usage, mandate_id: payment_data.mandate_id.clone(), @@ -1344,7 +1529,7 @@ impl TryFrom> for types::CompleteAuthoriz confirm: payment_data.payment_attempt.confirm, statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, capture_method: payment_data.payment_attempt.capture_method, - amount: payment_data.amount.into(), + amount, currency: payment_data.currency, browser_info, email: payment_data.email, @@ -1352,6 +1537,8 @@ impl TryFrom> for types::CompleteAuthoriz connector_transaction_id: payment_data.payment_attempt.connector_transaction_id, redirect_response, connector_meta: payment_data.payment_attempt.connector_metadata, + complete_authorize_url, + metadata: payment_data.payment_intent.metadata, }) } } @@ -1409,12 +1596,17 @@ impl TryFrom> for types::PaymentsPreProce .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "browser_info", })?; + let amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount) + .unwrap_or(payment_data.amount.into()); Ok(Self { payment_method_data, email: payment_data.email, currency: Some(payment_data.currency), - amount: Some(payment_data.amount.into()), + amount: Some(amount), payment_method_type: payment_data.payment_attempt.payment_method_type, setup_mandate_details: payment_data.setup_mandate, capture_method: payment_data.payment_attempt.capture_method, @@ -1424,6 +1616,8 @@ 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, + redirect_response: None, }) } } diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index f420a4b87a75..41a8850ab3d8 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -1,10 +1,21 @@ -use std::collections::HashMap; +use std::{collections::HashMap, num::TryFromIntError}; +use api_models::{payment_methods::SurchargeDetailsResponse, payments::RequestSurchargeDetails}; +use common_utils::{consts, errors::CustomResult, ext_traits::Encode, types as common_types}; +use data_models::payments::payment_attempt::PaymentAttempt; +use diesel_models::business_profile::BusinessProfile; use error_stack::{IntoReport, ResultExt}; +use redis_interface::errors::RedisError; +use router_env::{instrument, tracing}; use crate::{ + consts as router_consts, core::errors::{self, RouterResult}, - types::storage::{self, enums as storage_enums}, + routes::AppState, + types::{ + storage::{self, enums as storage_enums}, + transformers::ForeignTryFrom, + }, }; #[derive(Clone, Debug)] @@ -116,7 +127,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 } @@ -164,3 +175,198 @@ impl MultipleCaptureData { .collect() } } + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct SurchargeDetails { + /// original_amount + pub original_amount: i64, + /// surcharge value + pub surcharge: common_types::Surcharge, + /// tax on surcharge value + pub tax_on_surcharge: + Option>, + /// surcharge amount for this payment + pub surcharge_amount: i64, + /// tax on surcharge amount for this payment + pub tax_on_surcharge_amount: i64, + /// sum of original amount, + pub final_amount: i64, +} + +impl From<(&RequestSurchargeDetails, &PaymentAttempt)> for SurchargeDetails { + fn from( + (request_surcharge_details, payment_attempt): (&RequestSurchargeDetails, &PaymentAttempt), + ) -> Self { + let surcharge_amount = request_surcharge_details.surcharge_amount; + let tax_on_surcharge_amount = request_surcharge_details.tax_amount.unwrap_or(0); + Self { + original_amount: payment_attempt.amount, + surcharge: common_types::Surcharge::Fixed(request_surcharge_details.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount: payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount, + } + } +} + +impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsResponse { + type Error = TryFromIntError; + fn foreign_try_from( + (surcharge_details, payment_attempt): (&SurchargeDetails, &PaymentAttempt), + ) -> Result { + let currency = payment_attempt.currency.unwrap_or_default(); + let display_surcharge_amount = + currency.to_currency_base_unit_asf64(surcharge_details.surcharge_amount)?; + let display_tax_on_surcharge_amount = + currency.to_currency_base_unit_asf64(surcharge_details.tax_on_surcharge_amount)?; + let display_final_amount = + currency.to_currency_base_unit_asf64(surcharge_details.final_amount)?; + let display_total_surcharge_amount = currency.to_currency_base_unit_asf64( + surcharge_details.surcharge_amount + surcharge_details.tax_on_surcharge_amount, + )?; + Ok(Self { + surcharge: surcharge_details.surcharge.clone().into(), + tax_on_surcharge: surcharge_details.tax_on_surcharge.clone().map(Into::into), + display_surcharge_amount, + display_tax_on_surcharge_amount, + display_total_surcharge_amount, + display_final_amount, + }) + } +} + +impl SurchargeDetails { + pub fn is_request_surcharge_matching( + &self, + request_surcharge_details: RequestSurchargeDetails, + ) -> bool { + 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(Eq, Hash, PartialEq, Clone, Debug, strum::Display)] +pub enum SurchargeKey { + Token(String), + PaymentMethodData( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), +} + +#[derive(Clone, Debug)] +pub struct SurchargeMetadata { + surcharge_results: HashMap, + pub payment_attempt_id: String, +} + +impl SurchargeMetadata { + pub fn new(payment_attempt_id: String) -> Self { + Self { + surcharge_results: HashMap::new(), + payment_attempt_id, + } + } + pub fn is_empty_result(&self) -> bool { + self.surcharge_results.is_empty() + } + pub fn get_surcharge_results_size(&self) -> usize { + self.surcharge_results.len() + } + pub fn insert_surcharge_details( + &mut self, + surcharge_key: SurchargeKey, + surcharge_details: SurchargeDetails, + ) { + self.surcharge_results + .insert(surcharge_key, surcharge_details); + } + pub fn get_surcharge_details(&self, surcharge_key: SurchargeKey) -> Option<&SurchargeDetails> { + self.surcharge_results.get(&surcharge_key) + } + pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { + format!("surcharge_metadata_{}", payment_attempt_id) + } + pub fn get_individual_surcharge_key_value_pairs(&self) -> Vec<(String, SurchargeDetails)> { + self.surcharge_results + .iter() + .map(|(surcharge_key, surcharge_details)| { + let key = Self::get_surcharge_details_redis_hashset_key(surcharge_key); + (key, surcharge_details.to_owned()) + }) + .collect() + } + pub fn get_surcharge_details_redis_hashset_key(surcharge_key: &SurchargeKey) -> String { + match surcharge_key { + SurchargeKey::Token(token) => { + format!("token_{}", token) + } + SurchargeKey::PaymentMethodData(payment_method, payment_method_type, card_network) => { + if let Some(card_network) = card_network { + format!( + "{}_{}_{}", + payment_method, payment_method_type, card_network + ) + } else { + format!("{}_{}", payment_method, payment_method_type) + } + } + } + } + #[instrument(skip_all)] + pub async fn persist_individual_surcharge_details_in_redis( + &self, + state: &AppState, + business_profile: &BusinessProfile, + ) -> RouterResult<()> { + if !self.is_empty_result() { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + let redis_key = Self::get_surcharge_metadata_redis_key(&self.payment_attempt_id); + + let mut value_list = Vec::with_capacity(self.get_surcharge_results_size()); + for (key, value) in self.get_individual_surcharge_key_value_pairs().into_iter() { + value_list.push(( + key, + Encode::::encode_to_string_of_json(&value) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode to string of json")?, + )); + } + let intent_fulfillment_time = business_profile + .intent_fulfillment_time + .unwrap_or(router_consts::DEFAULT_FULFILLMENT_TIME); + redis_conn + .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to write to redis")?; + } + Ok(()) + } + + #[instrument(skip_all)] + pub async fn get_individual_surcharge_detail_from_redis( + state: &AppState, + surcharge_key: SurchargeKey, + payment_attempt_id: &str, + ) -> CustomResult { + let redis_conn = state + .store + .get_redis_conn() + .attach_printable("Failed to get redis connection")?; + let redis_key = Self::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = Self::get_surcharge_details_redis_hashset_key(&surcharge_key); + redis_conn + .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") + .await + } +} 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 c1e00b9b8000..1ab24023bdb8 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -1,4 +1,7 @@ -use common_utils::{errors::CustomResult, ext_traits::ValueExt}; +use common_utils::{ + errors::CustomResult, + ext_traits::{AsyncExt, StringExt, ValueExt}, +}; use diesel_models::encryption::Encryption; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface, Secret}; @@ -16,6 +19,7 @@ use crate::{ }, db::StorageInterface, routes::AppState, + services, types::{ api::{self, enums as api_enums}, domain::{ @@ -40,28 +44,50 @@ pub async fn make_payout_method_data<'a>( merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult> { let db = &*state.store; + let certain_payout_type = payout_type.get_required_value("payout_type")?.to_owned(); let hyperswitch_token = if let Some(payout_token) = payout_token { - let key = format!( - "pm_token_{}_{}_hyperswitch", - payout_token, - api_enums::PaymentMethod::foreign_from( - payout_type.get_required_value("payout_type")?.to_owned() - ) - ); - - let redis_conn = state - .store - .get_redis_conn() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get redis connection")?; - - let hyperswitch_token_option = redis_conn - .get_key::>(&key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch the token from redis")?; + if payout_token.starts_with("temporary_token_") { + Some(payout_token.to_string()) + } else { + let key = format!( + "pm_token_{}_{}_hyperswitch", + payout_token, + api_enums::PaymentMethod::foreign_from(certain_payout_type) + ); + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; - hyperswitch_token_option.or(Some(payout_token.to_string())) + let hyperswitch_token = redis_conn + .get_key::>(&key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch the token from redis")? + .ok_or(error_stack::Report::new( + errors::ApiErrorResponse::UnprocessableEntity { + message: "Token is invalid or expired".to_owned(), + }, + ))?; + let payment_token_data = hyperswitch_token + .clone() + .parse_struct("PaymentTokenData") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to deserialize hyperswitch token data")?; + + let payment_token = match payment_token_data { + storage::PaymentTokenData::PermanentCard(storage::CardTokenData { token }) => { + Some(token) + } + storage::PaymentTokenData::TemporaryGeneric(storage::GenericTokenData { + token, + }) => Some(token), + _ => None, + }; + payment_token.or(Some(payout_token.to_string())) + } } else { None }; @@ -69,8 +95,10 @@ pub async fn make_payout_method_data<'a>( match (payout_method_data.to_owned(), hyperswitch_token) { // Get operation (None, Some(payout_token)) => { - let (pm, supplementary_data) = - vault::Vault::get_payout_method_data_from_temporary_locker( + if payout_token.starts_with("temporary_token_") + || certain_payout_type == api_enums::PayoutType::Bank + { + let (pm, supplementary_data) = vault::Vault::get_payout_method_data_from_temporary_locker( state, &payout_token, merchant_key_store, @@ -79,15 +107,33 @@ pub async fn make_payout_method_data<'a>( .attach_printable( "Payout method for given token not found or there was a problem fetching it", )?; - utils::when( - supplementary_data - .customer_id - .ne(&Some(customer_id.to_owned())), - || { - Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payout method and customer passed in payout are not same".into() }) - }, - )?; - Ok(pm) + utils::when( + supplementary_data + .customer_id + .ne(&Some(customer_id.to_owned())), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payout method and customer passed in payout are not same".into() }) + }, + )?; + Ok(pm) + } else { + let resp = cards::get_card_from_locker( + state, + customer_id, + merchant_id, + payout_token.as_ref(), + ) + .await + .attach_printable("Payout method [card] could not be fetched from HS locker")?; + Ok(Some({ + api::PayoutMethodData::Card(api::CardPayout { + card_number: resp.card_number, + expiry_month: resp.card_exp_month, + expiry_year: resp.card_exp_year, + card_holder_name: resp.name_on_card, + }) + })) + } } // Create / Update operation @@ -131,32 +177,37 @@ pub async fn save_payout_data_to_locker( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { - let (locker_req, card_details, payment_method_type) = match payout_method_data { + let (locker_req, card_details, bank_details, payment_method_type) = match payout_method_data { api_models::payouts::PayoutMethodData::Card(card) => { let card_detail = api::CardDetail { card_number: card.card_number.to_owned(), - card_holder_name: Some(card.card_holder_name.to_owned()), + card_holder_name: card.card_holder_name.to_owned(), card_exp_month: card.expiry_month.to_owned(), card_exp_year: card.expiry_year.to_owned(), nick_name: None, + card_issuing_country: None, + card_network: None, + card_issuer: None, + card_type: None, }; let payload = StoreLockerReq::LockerCard(StoreCardReq { merchant_id: &merchant_account.merchant_id, merchant_customer_id: payout_attempt.customer_id.to_owned(), card: transformers::Card { card_number: card.card_number.to_owned(), - name_on_card: Some(card.card_holder_name.to_owned()), + name_on_card: card.card_holder_name.to_owned(), card_exp_month: card.expiry_month.to_owned(), card_exp_year: card.expiry_year.to_owned(), card_brand: None, card_isin: None, nick_name: None, }, - card_reference: None, + requestor_card_reference: None, }); ( payload, Some(card_detail), + None, api_enums::PaymentMethodType::Debit, ) } @@ -191,6 +242,7 @@ pub async fn save_payout_data_to_locker( ( payload, None, + Some(bank.to_owned()), api_enums::PaymentMethodType::foreign_from(bank.to_owned()), ) } @@ -220,20 +272,65 @@ pub async fn save_payout_data_to_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error updating payouts in saved payout method")?; - let pm_data = api::payment_methods::PaymentMethodsData::Card( - api::payment_methods::CardDetailsPaymentMethod { - last4_digits: card_details - .as_ref() - .map(|c| c.card_number.clone().get_last4()), - issuer_country: None, - expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()), - expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()), - nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()), - card_holder_name: card_details - .as_ref() - .and_then(|c| c.card_holder_name.clone()), - }, - ); + // fetch card info from db + let card_isin = card_details + .as_ref() + .map(|c| c.card_number.clone().get_card_isin()); + + let pm_data = card_isin + .clone() + .async_and_then(|card_isin| async move { + db.get_card_info(&card_isin) + .await + .map_err(|error| services::logger::warn!(card_info_error=?error)) + .ok() + }) + .await + .flatten() + .map(|card_info| { + api::payment_methods::PaymentMethodsData::Card( + api::payment_methods::CardDetailsPaymentMethod { + last4_digits: card_details + .as_ref() + .map(|c| c.card_number.clone().get_last4()), + issuer_country: card_info.card_issuing_country, + expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()), + expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()), + nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()), + card_holder_name: card_details + .as_ref() + .and_then(|c| c.card_holder_name.clone()), + + card_isin: card_isin.clone(), + card_issuer: card_info.card_issuer, + card_network: card_info.card_network, + card_type: card_info.card_type, + saved_to_locker: true, + }, + ) + }) + .unwrap_or_else(|| { + api::payment_methods::PaymentMethodsData::Card( + api::payment_methods::CardDetailsPaymentMethod { + last4_digits: card_details + .as_ref() + .map(|c| c.card_number.clone().get_last4()), + issuer_country: None, + expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()), + expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()), + nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()), + card_holder_name: card_details + .as_ref() + .and_then(|c| c.card_holder_name.clone()), + + card_isin: card_isin.clone(), + card_issuer: None, + card_network: None, + card_type: None, + saved_to_locker: true, + }, + ) + }); let card_details_encrypted = cards::create_encrypted_payment_method_data(key_store, Some(pm_data)).await; @@ -244,6 +341,7 @@ pub async fn save_payout_data_to_locker( payment_method_type: Some(payment_method_type), payment_method_issuer: None, payment_method_issuer_code: None, + bank_transfer: bank_details, card: card_details, metadata: None, customer_id: Some(payout_attempt.customer_id.to_owned()), 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/pm_auth.rs b/crates/router/src/core/pm_auth.rs new file mode 100644 index 000000000000..9f70cc6baeec --- /dev/null +++ b/crates/router/src/core/pm_auth.rs @@ -0,0 +1,758 @@ +use std::{collections::HashMap, str::FromStr}; + +use api_models::{ + enums, + payment_methods::{self, BankAccountAccessCreds}, + payments::{AddressDetails, BankDebitBilling, BankDebitData, PaymentMethodData}, +}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::{self, decrypt::VaultFetch}; +use hex; +pub mod helpers; +pub mod transformers; + +use common_utils::{ + consts, + crypto::{HmacSha256, SignMessage}, + ext_traits::AsyncExt, + generate_id, +}; +use data_models::payments::PaymentIntent; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +pub use external_services::kms; +use helpers::PaymentAuthConnectorDataExt; +use masking::{ExposeInterface, PeekInterface}; +use pm_auth::{ + connector::plaid::transformers::PlaidAuthType, + types::{ + self as pm_auth_types, + api::{ + auth_service::{BankAccountCredentials, ExchangeToken, LinkToken}, + BoxedConnectorIntegration, PaymentAuthConnectorData, + }, + }, +}; + +use crate::{ + core::{ + errors::{self, ApiErrorResponse, RouterResponse, RouterResult, StorageErrorExt}, + payment_methods::cards, + payments::helpers as oss_helpers, + pm_auth::helpers::{self as pm_auth_helpers}, + }, + db::StorageInterface, + logger, + routes::AppState, + services::{ + pm_auth::{self as pm_auth_services}, + ApplicationResponse, + }, + types::{ + self, + domain::{self, types::decrypt}, + storage, + transformers::ForeignTryFrom, + }, + utils::ext_traits::OptionExt, +}; + +pub async fn create_link_token( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + payload: api_models::pm_auth::LinkTokenCreateRequest, +) -> RouterResponse { + let db = &*state.store; + + let redis_conn = db + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let pm_auth_key = format!("pm_auth_{}", payload.payment_id); + + let pm_auth_configs = redis_conn + .get_and_deserialize_key::>( + pm_auth_key.as_str(), + "Vec", + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get payment method auth choices from redis")?; + + let selected_config = pm_auth_configs + .into_iter() + .find(|config| { + config.payment_method == payload.payment_method + && config.payment_method_type == payload.payment_method_type + }) + .ok_or(ApiErrorResponse::GenericNotFoundError { + message: "payment method auth connector name not found".to_string(), + }) + .into_report()?; + + let connector_name = selected_config.connector_name.as_str(); + + let connector = PaymentAuthConnectorData::get_connector_by_name(connector_name)?; + let connector_integration: BoxedConnectorIntegration< + '_, + LinkToken, + pm_auth_types::LinkTokenRequest, + pm_auth_types::LinkTokenResponse, + > = connector.connector.get_connector_integration(); + + let payment_intent = oss_helpers::verify_payment_intent_time_and_client_secret( + &*state.store, + &merchant_account, + payload.client_secret, + ) + .await?; + + let billing_country = payment_intent + .as_ref() + .async_map(|pi| async { + oss_helpers::get_address_by_id( + &*state.store, + pi.billing_address_id.clone(), + &key_store, + pi.payment_id.clone(), + merchant_account.merchant_id.clone(), + merchant_account.storage_scheme, + ) + .await + }) + .await + .transpose()? + .flatten() + .and_then(|address| address.country) + .map(|country| country.to_string()); + + let merchant_connector_account = state + .store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_account.merchant_id.as_str(), + &selected_config.mca_id, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + })?; + + let auth_type = helpers::get_connector_auth_type(merchant_connector_account)?; + + let router_data = pm_auth_types::LinkTokenRouterData { + flow: std::marker::PhantomData, + merchant_id: Some(merchant_account.merchant_id), + connector: Some(connector_name.to_string()), + request: pm_auth_types::LinkTokenRequest { + client_name: "HyperSwitch".to_string(), + country_codes: Some(vec![billing_country.ok_or( + errors::ApiErrorResponse::MissingRequiredField { + field_name: "billing_country", + }, + )?]), + language: payload.language, + user_info: payment_intent.and_then(|pi| pi.customer_id), + }, + response: Ok(pm_auth_types::LinkTokenResponse { + link_token: "".to_string(), + }), + connector_http_status_code: None, + connector_auth_type: auth_type, + }; + + let connector_resp = pm_auth_services::execute_connector_processing_step( + state.as_ref(), + connector_integration, + &router_data, + &connector.connector_name, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while calling link token creation connector api")?; + + let link_token_resp = + connector_resp + .response + .map_err(|err| ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector.connector_name.to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + + let response = api_models::pm_auth::LinkTokenCreateResponse { + link_token: link_token_resp.link_token, + connector: connector.connector_name.to_string(), + }; + + Ok(ApplicationResponse::Json(response)) +} + +impl ForeignTryFrom<&types::ConnectorAuthType> for PlaidAuthType { + type Error = errors::ConnectorError; + + fn foreign_try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { api_key, key1 } => { + Ok::(Self { + client_id: api_key.to_owned(), + secret: key1.to_owned(), + }) + } + _ => Err(errors::ConnectorError::FailedToObtainAuthType), + } + } +} + +pub async fn exchange_token_core( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + payload: api_models::pm_auth::ExchangeTokenCreateRequest, +) -> RouterResponse<()> { + let db = &*state.store; + + let config = get_selected_config_from_redis(db, &payload).await?; + + let connector_name = config.connector_name.as_str(); + + let connector = + pm_auth_types::api::PaymentAuthConnectorData::get_connector_by_name(connector_name)?; + + let merchant_connector_account = state + .store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_account.merchant_id.as_str(), + &config.mca_id, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + })?; + + let auth_type = helpers::get_connector_auth_type(merchant_connector_account.clone())?; + + let access_token = get_access_token_from_exchange_api( + &connector, + connector_name, + &payload, + &auth_type, + &state, + ) + .await?; + + let bank_account_details_resp = get_bank_account_creds( + connector, + &merchant_account, + connector_name, + &access_token, + auth_type, + &state, + None, + ) + .await?; + + Box::pin(store_bank_details_in_payment_methods( + key_store, + payload, + merchant_account, + state, + bank_account_details_resp, + (connector_name, access_token), + merchant_connector_account.merchant_connector_id, + )) + .await?; + + Ok(ApplicationResponse::StatusOk) +} + +async fn store_bank_details_in_payment_methods( + key_store: domain::MerchantKeyStore, + payload: api_models::pm_auth::ExchangeTokenCreateRequest, + merchant_account: domain::MerchantAccount, + state: AppState, + bank_account_details_resp: pm_auth_types::BankAccountCredentialsResponse, + connector_details: (&str, String), + mca_id: String, +) -> RouterResult<()> { + let key = key_store.key.get_inner().peek(); + let db = &*state.clone().store; + let (connector_name, access_token) = connector_details; + + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id( + &payload.payment_id, + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(ApiErrorResponse::PaymentNotFound)?; + + let customer_id = payment_intent + .customer_id + .ok_or(ApiErrorResponse::CustomerNotFound)?; + + let payment_methods = db + .find_payment_method_by_customer_id_merchant_id_list( + &customer_id, + &merchant_account.merchant_id, + ) + .await + .change_context(ApiErrorResponse::InternalServerError)?; + + let mut hash_to_payment_method: HashMap< + String, + ( + storage::PaymentMethod, + payment_methods::PaymentMethodDataBankCreds, + ), + > = HashMap::new(); + + for pm in payment_methods { + if pm.payment_method == enums::PaymentMethod::BankDebit { + let bank_details_pm_data = decrypt::( + pm.payment_method_data.clone(), + key, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("unable to decrypt bank account details")? + .map(|x| x.into_inner().expose()) + .map(|v| { + serde_json::from_value::(v) + .into_report() + .change_context(errors::StorageError::DeserializationFailed) + .attach_printable("Failed to deserialize Payment Method Auth config") + }) + .transpose() + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }) + .and_then(|pmd| match pmd { + payment_methods::PaymentMethodsData::BankDetails(bank_creds) => Some(bank_creds), + _ => None, + }) + .ok_or(ApiErrorResponse::InternalServerError)?; + + hash_to_payment_method.insert( + bank_details_pm_data.hash.clone(), + (pm, bank_details_pm_data), + ); + } + } + + let pm_auth_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while creating client")?; + + #[cfg(feature = "hashicorp-vault")] + let output = masking::Secret::new(state.conf.payment_method_auth.pm_auth_key.clone()) + .fetch_inner::(client) + .await + .change_context(ApiErrorResponse::InternalServerError)? + .expose(); + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.payment_method_auth.pm_auth_key.clone(); + + Ok::<_, error_stack::Report>(output) + } + .await?; + + #[cfg(feature = "kms")] + let pm_auth_key = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(pm_auth_key) + .await + .change_context(ApiErrorResponse::InternalServerError)?; + + let mut update_entries: Vec<(storage::PaymentMethod, storage::PaymentMethodUpdate)> = + Vec::new(); + let mut new_entries: Vec = Vec::new(); + + for creds in bank_account_details_resp.credentials { + let hash_string = format!("{}-{}", creds.account_number, creds.routing_number); + let generated_hash = hex::encode( + HmacSha256::sign_message(&HmacSha256, pm_auth_key.as_bytes(), hash_string.as_bytes()) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to sign the message")?, + ); + + let contains_account = hash_to_payment_method.get(&generated_hash); + let mut pmd = payment_methods::PaymentMethodDataBankCreds { + mask: creds + .account_number + .chars() + .rev() + .take(4) + .collect::() + .chars() + .rev() + .collect::(), + hash: generated_hash, + account_type: creds.account_type, + account_name: creds.account_name, + payment_method_type: creds.payment_method_type, + connector_details: vec![payment_methods::BankAccountConnectorDetails { + connector: connector_name.to_string(), + mca_id: mca_id.clone(), + access_token: payment_methods::BankAccountAccessCreds::AccessToken( + access_token.clone(), + ), + account_id: creds.account_id, + }], + }; + + if let Some((pm, details)) = contains_account { + pmd.connector_details.extend( + details + .connector_details + .clone() + .into_iter() + .filter(|conn| conn.mca_id != mca_id), + ); + + let payment_method_data = payment_methods::PaymentMethodsData::BankDetails(pmd); + let encrypted_data = + cards::create_encrypted_payment_method_data(&key_store, Some(payment_method_data)) + .await + .ok_or(ApiErrorResponse::InternalServerError)?; + let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate { + payment_method_data: Some(encrypted_data), + }; + + update_entries.push((pm.clone(), pm_update)); + } else { + let payment_method_data = payment_methods::PaymentMethodsData::BankDetails(pmd); + let encrypted_data = + cards::create_encrypted_payment_method_data(&key_store, Some(payment_method_data)) + .await + .ok_or(ApiErrorResponse::InternalServerError)?; + let pm_id = generate_id(consts::ID_LENGTH, "pm"); + let pm_new = storage::PaymentMethodNew { + customer_id: customer_id.clone(), + merchant_id: merchant_account.merchant_id.clone(), + payment_method_id: pm_id, + payment_method: enums::PaymentMethod::BankDebit, + payment_method_type: Some(creds.payment_method_type), + payment_method_issuer: None, + scheme: None, + metadata: None, + payment_method_data: Some(encrypted_data), + ..storage::PaymentMethodNew::default() + }; + + new_entries.push(pm_new); + }; + } + + store_in_db(update_entries, new_entries, db).await?; + + Ok(()) +} + +async fn store_in_db( + update_entries: Vec<(storage::PaymentMethod, storage::PaymentMethodUpdate)>, + new_entries: Vec, + db: &dyn StorageInterface, +) -> RouterResult<()> { + let update_entries_futures = update_entries + .into_iter() + .map(|(pm, pm_update)| db.update_payment_method(pm, pm_update)) + .collect::>(); + + let new_entries_futures = new_entries + .into_iter() + .map(|pm_new| db.insert_payment_method(pm_new)) + .collect::>(); + + let update_futures = futures::future::join_all(update_entries_futures); + let new_futures = futures::future::join_all(new_entries_futures); + + let (update, new) = tokio::join!(update_futures, new_futures); + + let _ = update + .into_iter() + .map(|res| res.map_err(|err| logger::error!("Payment method storage failed {err:?}"))); + + let _ = new + .into_iter() + .map(|res| res.map_err(|err| logger::error!("Payment method storage failed {err:?}"))); + + Ok(()) +} + +pub async fn get_bank_account_creds( + connector: PaymentAuthConnectorData, + merchant_account: &domain::MerchantAccount, + connector_name: &str, + access_token: &str, + auth_type: pm_auth_types::ConnectorAuthType, + state: &AppState, + bank_account_id: Option, +) -> RouterResult { + let connector_integration_bank_details: BoxedConnectorIntegration< + '_, + BankAccountCredentials, + pm_auth_types::BankAccountCredentialsRequest, + pm_auth_types::BankAccountCredentialsResponse, + > = connector.connector.get_connector_integration(); + + let router_data_bank_details = pm_auth_types::BankDetailsRouterData { + flow: std::marker::PhantomData, + merchant_id: Some(merchant_account.merchant_id.clone()), + connector: Some(connector_name.to_string()), + request: pm_auth_types::BankAccountCredentialsRequest { + access_token: access_token.to_string(), + optional_ids: bank_account_id + .map(|id| pm_auth_types::BankAccountOptionalIDs { ids: vec![id] }), + }, + response: Ok(pm_auth_types::BankAccountCredentialsResponse { + credentials: Vec::new(), + }), + connector_http_status_code: None, + connector_auth_type: auth_type, + }; + + let bank_details_resp = pm_auth_services::execute_connector_processing_step( + state, + connector_integration_bank_details, + &router_data_bank_details, + &connector.connector_name, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while calling bank account details connector api")?; + + let bank_account_details_resp = + bank_details_resp + .response + .map_err(|err| ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector.connector_name.to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + + Ok(bank_account_details_resp) +} + +async fn get_access_token_from_exchange_api( + connector: &PaymentAuthConnectorData, + connector_name: &str, + payload: &api_models::pm_auth::ExchangeTokenCreateRequest, + auth_type: &pm_auth_types::ConnectorAuthType, + state: &AppState, +) -> RouterResult { + let connector_integration: BoxedConnectorIntegration< + '_, + ExchangeToken, + pm_auth_types::ExchangeTokenRequest, + pm_auth_types::ExchangeTokenResponse, + > = connector.connector.get_connector_integration(); + + let router_data = pm_auth_types::ExchangeTokenRouterData { + flow: std::marker::PhantomData, + merchant_id: None, + connector: Some(connector_name.to_string()), + request: pm_auth_types::ExchangeTokenRequest { + public_token: payload.public_token.clone(), + }, + response: Ok(pm_auth_types::ExchangeTokenResponse { + access_token: "".to_string(), + }), + connector_http_status_code: None, + connector_auth_type: auth_type.clone(), + }; + + let resp = pm_auth_services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + &connector.connector_name, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while calling exchange token connector api")?; + + let exchange_token_resp = + resp.response + .map_err(|err| ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector.connector_name.to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + + let access_token = exchange_token_resp.access_token; + Ok(access_token) +} + +async fn get_selected_config_from_redis( + db: &dyn StorageInterface, + payload: &api_models::pm_auth::ExchangeTokenCreateRequest, +) -> RouterResult { + let redis_conn = db + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let pm_auth_key = format!("pm_auth_{}", payload.payment_id); + + let pm_auth_configs = redis_conn + .get_and_deserialize_key::>( + pm_auth_key.as_str(), + "Vec", + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get payment method auth choices from redis")?; + + let selected_config = pm_auth_configs + .iter() + .find(|conf| { + conf.payment_method == payload.payment_method + && conf.payment_method_type == payload.payment_method_type + }) + .ok_or(ApiErrorResponse::GenericNotFoundError { + message: "connector name not found".to_string(), + }) + .into_report()? + .clone(); + + Ok(selected_config) +} + +pub async fn retrieve_payment_method_from_auth_service( + state: &AppState, + key_store: &domain::MerchantKeyStore, + auth_token: &payment_methods::BankAccountConnectorDetails, + payment_intent: &PaymentIntent, + customer: &Option, +) -> RouterResult> { + let db = state.store.as_ref(); + + let connector = pm_auth_types::api::PaymentAuthConnectorData::get_connector_by_name( + auth_token.connector.as_str(), + )?; + + let merchant_account = db + .find_merchant_account_by_merchant_id(&payment_intent.merchant_id, key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let mca = db + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &payment_intent.merchant_id, + &auth_token.mca_id, + key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: auth_token.mca_id.clone(), + }) + .attach_printable( + "error while fetching merchant_connector_account from merchant_id and connector name", + )?; + + let auth_type = pm_auth_helpers::get_connector_auth_type(mca)?; + + let BankAccountAccessCreds::AccessToken(access_token) = &auth_token.access_token; + + let bank_account_creds = get_bank_account_creds( + connector, + &merchant_account, + &auth_token.connector, + access_token, + auth_type, + state, + Some(auth_token.account_id.clone()), + ) + .await?; + + logger::debug!("bank_creds: {:?}", bank_account_creds); + + let bank_account = bank_account_creds + .credentials + .first() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Bank account details not found")?; + + let mut bank_type = None; + if let Some(account_type) = bank_account.account_type.clone() { + bank_type = api_models::enums::BankType::from_str(account_type.as_str()) + .map_err(|error| logger::error!(%error,"unable to parse account_type {account_type:?}")) + .ok(); + } + + let address = oss_helpers::get_address_by_id( + &*state.store, + payment_intent.billing_address_id.clone(), + key_store, + payment_intent.payment_id.clone(), + merchant_account.merchant_id.clone(), + merchant_account.storage_scheme, + ) + .await?; + + let name = address + .as_ref() + .and_then(|addr| addr.first_name.clone().map(|name| name.into_inner())); + + let address_details = address.clone().map(|addr| { + let line1 = addr.line1.map(|line1| line1.into_inner()); + let line2 = addr.line2.map(|line2| line2.into_inner()); + let line3 = addr.line3.map(|line3| line3.into_inner()); + let zip = addr.zip.map(|zip| zip.into_inner()); + let state = addr.state.map(|state| state.into_inner()); + let first_name = addr.first_name.map(|first_name| first_name.into_inner()); + let last_name = addr.last_name.map(|last_name| last_name.into_inner()); + + AddressDetails { + city: addr.city, + country: addr.country, + line1, + line2, + line3, + zip, + state, + first_name, + last_name, + } + }); + + let email = customer + .as_ref() + .and_then(|customer| customer.email.clone()) + .map(common_utils::pii::Email::from) + .get_required_value("email")?; + + let payment_method_data = PaymentMethodData::BankDebit(BankDebitData::AchBankDebit { + billing_details: BankDebitBilling { + name: name.unwrap_or_default(), + email, + address: address_details, + }, + account_number: masking::Secret::new(bank_account.account_number.clone()), + routing_number: masking::Secret::new(bank_account.routing_number.clone()), + card_holder_name: None, + bank_account_holder_name: None, + bank_name: None, + bank_type, + bank_holder_type: None, + }); + + Ok(Some((payment_method_data, enums::PaymentMethod::BankDebit))) +} diff --git a/crates/router/src/core/pm_auth/helpers.rs b/crates/router/src/core/pm_auth/helpers.rs new file mode 100644 index 000000000000..43d30705a803 --- /dev/null +++ b/crates/router/src/core/pm_auth/helpers.rs @@ -0,0 +1,33 @@ +use common_utils::ext_traits::ValueExt; +use error_stack::{IntoReport, ResultExt}; +use pm_auth::types::{self as pm_auth_types, api::BoxedPaymentAuthConnector}; + +use crate::{ + core::errors::{self, ApiErrorResponse}, + types::{self, domain, transformers::ForeignTryFrom}, +}; + +pub trait PaymentAuthConnectorDataExt { + fn get_connector_by_name(name: &str) -> errors::CustomResult + where + Self: Sized; + fn convert_connector( + connector_name: pm_auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult; +} + +pub fn get_connector_auth_type( + merchant_connector_account: domain::MerchantConnectorAccount, +) -> errors::CustomResult { + let auth_type: types::ConnectorAuthType = merchant_connector_account + .connector_account_details + .parse_value("ConnectorAuthType") + .change_context(ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + pm_auth_types::ConnectorAuthType::foreign_try_from(auth_type) + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while converting ConnectorAuthType") +} diff --git a/crates/router/src/core/pm_auth/transformers.rs b/crates/router/src/core/pm_auth/transformers.rs new file mode 100644 index 000000000000..8a1369c2e02f --- /dev/null +++ b/crates/router/src/core/pm_auth/transformers.rs @@ -0,0 +1,18 @@ +use pm_auth::types::{self as pm_auth_types}; + +use crate::{core::errors, types, types::transformers::ForeignTryFrom}; + +impl ForeignTryFrom for pm_auth_types::ConnectorAuthType { + type Error = errors::ConnectorError; + fn foreign_try_from(auth_type: types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { api_key, key1 } => { + Ok::(Self::BodyKey { + client_id: api_key.to_owned(), + secret: key1.to_owned(), + }) + } + _ => Err(errors::ConnectorError::FailedToObtainAuthType), + } + } +} diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index a42e46ca62d5..4b1c33296e65 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 }; @@ -569,7 +610,11 @@ pub async fn validate_and_create_refund( ), })?; - validator::validate_refund_amount(payment_attempt.amount, &all_refunds, refund_amount) + let total_amount_captured = payment_intent + .amount_captured + .unwrap_or(payment_attempt.amount); + + validator::validate_refund_amount(total_amount_captured, &all_refunds, refund_amount) .change_context(errors::ApiErrorResponse::RefundAmountExceedsPaymentAmount)?; validator::validate_maximum_refund_against_payment_attempt( @@ -605,6 +650,7 @@ pub async fn validate_and_create_refund( .set_attempt_id(payment_attempt.attempt_id.clone()) .set_refund_reason(req.reason) .set_profile_id(payment_intent.profile_id.clone()) + .set_merchant_connector_id(payment_attempt.merchant_connector_id.clone()) .to_owned(); let refund = match db @@ -731,6 +777,7 @@ impl ForeignFrom for api::RefundResponse { created_at: Some(refund.created_at), updated_at: Some(refund.updated_at), connector: refund.connector, + merchant_connector_id: refund.merchant_connector_id, } } } @@ -888,8 +935,12 @@ pub async fn start_refund_workflow( refund_tracker: &storage::ProcessTracker, ) -> 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("EXECUTE_REFUND") => { + Box::pin(trigger_refund_execute_workflow(state, refund_tracker)).await + } + Some("SYNC_REFUND") => { + Box::pin(sync_refund_with_gateway_workflow(state, refund_tracker)).await + } _ => Err(errors::ProcessTrackerError::JobNotFound), } } @@ -1037,6 +1088,12 @@ pub async fn add_refund_sync_task( refund.refund_id ) })?; + metrics::TASKS_ADDED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes("flow", "Refund")], + ); + Ok(response) } @@ -1119,7 +1176,15 @@ pub async fn retry_refund_sync_task( get_refund_sync_process_schedule_time(db, &connector, &merchant_id, pt.retry_count).await?; match schedule_time { - Some(s_time) => pt.retry(db.as_scheduler(), s_time).await, + Some(s_time) => { + let retry_schedule = pt.retry(db.as_scheduler(), s_time).await; + metrics::TASKS_RESET_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes("flow", "Refund")], + ); + retry_schedule + } None => { pt.finish_with_status(db.as_scheduler(), "RETRIES_EXCEEDED".to_string()) .await diff --git a/crates/router/src/core/refunds/validator.rs b/crates/router/src/core/refunds/validator.rs index 6198a6f79a68..cae8f0494bee 100644 --- a/crates/router/src/core/refunds/validator.rs +++ b/crates/router/src/core/refunds/validator.rs @@ -17,7 +17,7 @@ pub const DEFAULT_LIMIT: i64 = 10; pub enum RefundValidationError { #[error("The payment attempt was not successful")] UnsuccessfulPaymentAttempt, - #[error("The refund amount exceeds the payment amount")] + #[error("The refund amount exceeds the amount captured")] RefundAmountExceedsPaymentAmount, #[error("The order has expired")] OrderExpired, @@ -40,7 +40,7 @@ pub fn validate_success_transaction( #[instrument(skip_all)] pub fn validate_refund_amount( - payment_attempt_amount: i64, // &storage::PaymentAttempt, + amount_captured: i64, all_refunds: &[storage::Refund], refund_amount: i64, ) -> CustomResult<(), RefundValidationError> { @@ -58,7 +58,7 @@ pub fn validate_refund_amount( .sum(); utils::when( - refund_amount > (payment_attempt_amount - total_refunded_amount), + refund_amount > (amount_captured - total_refunded_amount), || { Err(report!( RefundValidationError::RefundAmountExceedsPaymentAmount 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/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 6eec39f53bc6..c86e527b00d1 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -255,6 +255,8 @@ pub async fn update_business_profile_active_algorithm_ref( applepay_verified_domains: None, modified_at: None, is_recon_enabled: None, + payment_link_config: None, + session_expiry: None, }; db.update_business_profile_by_profile_id(current_business_profile, business_profile_update) .await 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..7050c9f00244 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,18 +1,180 @@ -use api_models::user as api; -use diesel_models::enums::UserStatus; +use api_models::user::{self as user_api, InviteMultipleUserResponse}; +#[cfg(feature = "email")] +use diesel_models::user_role::UserRoleUpdate; +use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew}; +#[cfg(feature = "email")] use error_stack::IntoReport; -use masking::{ExposeInterface, Secret}; +use error_stack::ResultExt; +use masking::ExposeInterface; +#[cfg(feature = "email")] use router_env::env; +#[cfg(feature = "email")] +use router_env::logger; -use super::errors::{UserErrors, UserResponse}; +use super::errors::{UserErrors, UserResponse, UserResult}; +#[cfg(feature = "email")] +use crate::services::email::types as email_types; use crate::{ - consts::user as consts, routes::AppState, services::ApplicationResponse, types::domain, + consts, + routes::AppState, + services::{authentication as auth, ApplicationResponse}, + types::domain, + utils, }; +pub mod dashboard_metadata; +#[cfg(feature = "dummy_connector")] +pub mod sample_data; +#[cfg(feature = "email")] +pub async fn signup_with_merchant_id( + state: AppState, + request: user_api::SignUpWithMerchantIdRequest, +) -> UserResponse { + let new_user = domain::NewUser::try_from(request.clone())?; + new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + + let email_contents = email_types::ResetPassword { + recipient_email: user_from_db.get_email().try_into()?, + user_name: domain::UserName::new(user_from_db.get_name())?, + settings: state.conf.clone(), + subject: "Get back to Hyperswitch - Reset Your Password Now", + }; + + 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); + Ok(ApplicationResponse::Json(user_api::AuthorizeResponse { + is_email_sent: send_email_result.is_ok(), + user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, + })) +} + +pub async fn signup( + state: AppState, + request: user_api::SignUpRequest, +) -> UserResponse { + let new_user = domain::NewUser::try_from(request)?; + new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, + )) +} + +pub async fn signin_without_invite_checks( + state: AppState, + request: user_api::SignInRequest, +) -> UserResponse { + let user_from_db: domain::UserFromStorage = state + .store + .find_user_by_email(request.email.clone().expose().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::InvalidCredentials) + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .into(); + + user_from_db.compare_password(request.password)?; + + let user_role = user_from_db.get_role_from_db(state.clone()).await?; + let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, + )) +} + +pub async fn signin( + state: AppState, + request: user_api::SignInRequest, +) -> UserResponse { + let user_from_db: domain::UserFromStorage = state + .store + .find_user_by_email(request.email.clone().expose().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::InvalidCredentials) + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .into(); + + user_from_db.compare_password(request.password)?; + + let signin_strategy = + if let Some(preferred_merchant_id) = user_from_db.get_preferred_merchant_id() { + let preferred_role = user_from_db + .get_role_from_db_by_merchant_id(&state, preferred_merchant_id.as_str()) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("User role with preferred_merchant_id not found")?; + domain::SignInWithRoleStrategyType::SingleRole(domain::SignInWithSingleRoleStrategy { + user: user_from_db, + user_role: preferred_role, + }) + } else { + let user_roles = user_from_db.get_roles_from_db(&state).await?; + domain::SignInWithRoleStrategyType::decide_signin_strategy_by_user_roles( + user_from_db, + user_roles, + ) + .await? + }; + + Ok(ApplicationResponse::Json( + signin_strategy.get_signin_response(&state).await?, + )) +} + +#[cfg(feature = "email")] 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()) @@ -20,24 +182,34 @@ pub async fn connect_account( if let Ok(found_user) = find_user { let user_from_db: domain::UserFromStorage = found_user.into(); + let user_role = user_from_db.get_role_from_db(state.clone()).await?; - user_from_db.compare_password(request.password)?; + let email_contents = email_types::MagicLink { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + user_name: domain::UserName::new(user_from_db.get_name())?, + subject: "Unlock Hyperswitch: Use Your Magic Link to Sign In", + }; - let user_role = user_from_db.get_role_from_db(state.clone()).await?; - let jwt_token = user_from_db - .get_jwt_auth_token(state.clone(), user_role.org_id) - .await?; + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .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(), - })); + logger::info!(?send_email_result); + + return Ok(ApplicationResponse::Json( + user_api::ConnectAccountResponse { + is_email_sent: send_email_result.is_ok(), + user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, + }, + )); } else if find_user + .as_ref() .map_err(|e| e.current_context().is_db_not_found()) .err() .unwrap_or(false) @@ -58,24 +230,886 @@ 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?; - let jwt_token = user_from_db - .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(), - })); + let email_contents = email_types::VerifyEmail { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + subject: "Welcome to the Hyperswitch community!", + }; + + 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 { + is_email_sent: send_email_result.is_ok(), + user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, + }, + )); + } else { + Err(find_user + .err() + .map(|e| e.change_context(UserErrors::InternalServerError)) + .unwrap_or(UserErrors::InternalServerError.into())) + } +} + +pub async fn signout(state: AppState, user_from_token: auth::UserFromToken) -> UserResponse<()> { + auth::blacklist::insert_user_in_blacklist(&state, &user_from_token.user_id).await?; + Ok(ApplicationResponse::StatusOk) +} + +pub async fn change_password( + state: AppState, + request: user_api::ChangePasswordRequest, + user_from_token: auth::UserFromToken, +) -> UserResponse<()> { + let user: domain::UserFromStorage = state + .store + .find_user_by_id(&user_from_token.user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + user.compare_password(request.old_password.to_owned()) + .change_context(UserErrors::InvalidOldPassword)?; + + if request.old_password == request.new_password { + return Err(UserErrors::ChangePasswordError.into()); + } + let new_password = domain::UserPassword::new(request.new_password)?; + + let new_password_hash = + utils::user::password::generate_password_hash(new_password.get_secret())?; + + let _ = state + .store + .update_user_by_user_id( + user.get_user_id(), + diesel_models::user::UserUpdate::AccountUpdate { + name: None, + password: Some(new_password_hash), + is_verified: None, + preferred_merchant_id: None, + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + Ok(ApplicationResponse::StatusOk) +} + +#[cfg(feature = "email")] +pub async fn forgot_password( + state: AppState, + request: user_api::ForgotPasswordRequest, +) -> UserResponse<()> { + let user_email = domain::UserEmail::from_pii_email(request.email)?; + + let user_from_db = state + .store + .find_user_by_email(user_email.get_secret().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::UserNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + }) + .map(domain::UserFromStorage::from)?; + + let email_contents = email_types::ResetPassword { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + user_name: domain::UserName::new(user_from_db.get_name())?, + subject: "Get back to Hyperswitch - Reset Your Password Now", + }; + + state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .map_err(|e| e.change_context(UserErrors::InternalServerError))?; + + Ok(ApplicationResponse::StatusOk) +} + +#[cfg(feature = "email")] +pub async fn reset_password( + state: AppState, + request: user_api::ResetPasswordRequest, +) -> UserResponse<()> { + let token = + auth::decode_jwt::(request.token.expose().as_str(), &state) + .await + .change_context(UserErrors::LinkInvalid)?; + + let password = domain::UserPassword::new(request.password)?; + + let hash_password = utils::user::password::generate_password_hash(password.get_secret())?; + + let user = state + .store + .update_user_by_email( + token.get_email(), + storage_user::UserUpdate::AccountUpdate { + name: None, + password: Some(hash_password), + is_verified: Some(true), + preferred_merchant_id: None, + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + if let Some(inviter_merchant_id) = token.get_merchant_id() { + let update_status_result = state + .store + .update_user_role_by_user_id_merchant_id( + user.user_id.clone().as_str(), + inviter_merchant_id, + UserRoleUpdate::UpdateStatus { + status: UserStatus::Active, + modified_by: user.user_id, + }, + ) + .await; + logger::info!(?update_status_result); + } + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn invite_user( + state: AppState, + request: user_api::InviteUserRequest, + user_from_token: auth::UserFromToken, +) -> UserResponse { + let inviter_user = state + .store + .find_user_by_id(user_from_token.user_id.as_str()) + .await + .change_context(UserErrors::InternalServerError)?; + + if inviter_user.email == request.email { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User Inviting themself"); + } + + utils::user_role::validate_role_id(request.role_id.as_str())?; + let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; + + let invitee_user = state + .store + .find_user_by_email(invitee_email.clone().get_secret().expose().as_str()) + .await; + + if let Ok(invitee_user) = invitee_user { + let invitee_user_from_db = domain::UserFromStorage::from(invitee_user); + + let now = common_utils::date_time::now(); + state + .store + .insert_user_role(UserRoleNew { + user_id: invitee_user_from_db.get_user_id().to_owned(), + merchant_id: user_from_token.merchant_id, + role_id: request.role_id, + org_id: user_from_token.org_id, + status: UserStatus::Active, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id, + created_at: now, + last_modified: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + Ok(ApplicationResponse::Json(user_api::InviteUserResponse { + is_email_sent: false, + password: None, + })) + } else if invitee_user + .as_ref() + .map_err(|e| e.current_context().is_db_not_found()) + .err() + .unwrap_or(false) + { + let new_user = domain::NewUser::try_from((request.clone(), user_from_token.clone()))?; + + new_user + .insert_user_in_db(state.store.as_ref()) + .await + .change_context(UserErrors::InternalServerError)?; + + let invitation_status = if cfg!(feature = "email") { + UserStatus::InvitationSent + } else { + UserStatus::Active + }; + + let now = common_utils::date_time::now(); + state + .store + .insert_user_role(UserRoleNew { + user_id: new_user.get_user_id().to_owned(), + merchant_id: user_from_token.merchant_id.clone(), + role_id: request.role_id, + org_id: user_from_token.org_id, + status: invitation_status, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id, + created_at: now, + last_modified: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + let is_email_sent; + #[cfg(feature = "email")] + { + let email_contents = email_types::InviteUser { + recipient_email: invitee_email, + user_name: domain::UserName::new(new_user.get_name())?, + settings: state.conf.clone(), + subject: "You have been invited to join Hyperswitch Community!", + merchant_id: user_from_token.merchant_id, + }; + 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); + is_email_sent = send_email_result.is_ok(); + } + #[cfg(not(feature = "email"))] + { + is_email_sent = false; + } + + Ok(ApplicationResponse::Json(user_api::InviteUserResponse { + is_email_sent, + password: if cfg!(not(feature = "email")) { + Some(new_user.get_password().get_secret()) + } else { + None + }, + })) + } else { + Err(UserErrors::InternalServerError.into()) + } +} + +pub async fn invite_multiple_user( + state: AppState, + user_from_token: auth::UserFromToken, + requests: Vec, +) -> UserResponse> { + if requests.len() > 10 { + return Err(UserErrors::MaxInvitationsError.into()) + .attach_printable("Number of invite requests must not exceed 10"); + } + + let responses = futures::future::join_all(requests.iter().map(|request| async { + match handle_invitation(&state, &user_from_token, request).await { + Ok(response) => response, + Err(error) => InviteMultipleUserResponse { + email: request.email.clone(), + is_email_sent: false, + password: None, + error: Some(error.current_context().get_error_message().to_string()), + }, + } + })) + .await; + + Ok(ApplicationResponse::Json(responses)) +} + +async fn handle_invitation( + state: &AppState, + user_from_token: &auth::UserFromToken, + request: &user_api::InviteUserRequest, +) -> UserResult { + let inviter_user = user_from_token.get_user(state.clone()).await?; + + if inviter_user.email == request.email { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User Inviting themself"); + } + + utils::user_role::validate_role_id(request.role_id.as_str())?; + let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; + let invitee_user = state + .store + .find_user_by_email(invitee_email.clone().get_secret().expose().as_str()) + .await; + + if let Ok(invitee_user) = invitee_user { + handle_existing_user_invitation(state, user_from_token, request, invitee_user.into()).await + } else if invitee_user + .as_ref() + .map_err(|e| e.current_context().is_db_not_found()) + .err() + .unwrap_or(false) + { + handle_new_user_invitation(state, user_from_token, request).await } else { Err(UserErrors::InternalServerError.into()) } } + +//TODO: send email +async fn handle_existing_user_invitation( + state: &AppState, + user_from_token: &auth::UserFromToken, + request: &user_api::InviteUserRequest, + invitee_user_from_db: domain::UserFromStorage, +) -> UserResult { + let now = common_utils::date_time::now(); + state + .store + .insert_user_role(UserRoleNew { + user_id: invitee_user_from_db.get_user_id().to_owned(), + merchant_id: user_from_token.merchant_id.clone(), + role_id: request.role_id.clone(), + org_id: user_from_token.org_id.clone(), + status: UserStatus::Active, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id.clone(), + created_at: now, + last_modified: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + Ok(InviteMultipleUserResponse { + email: request.email.clone(), + is_email_sent: false, + password: None, + error: None, + }) +} + +async fn handle_new_user_invitation( + state: &AppState, + user_from_token: &auth::UserFromToken, + request: &user_api::InviteUserRequest, +) -> UserResult { + let new_user = domain::NewUser::try_from((request.clone(), user_from_token.clone()))?; + + new_user + .insert_user_in_db(state.store.as_ref()) + .await + .change_context(UserErrors::InternalServerError)?; + + let invitation_status = if cfg!(feature = "email") { + UserStatus::InvitationSent + } else { + UserStatus::Active + }; + + let now = common_utils::date_time::now(); + state + .store + .insert_user_role(UserRoleNew { + user_id: new_user.get_user_id().to_owned(), + merchant_id: user_from_token.merchant_id.clone(), + role_id: request.role_id.clone(), + org_id: user_from_token.org_id.clone(), + status: invitation_status, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id.clone(), + created_at: now, + last_modified: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + let is_email_sent; + #[cfg(feature = "email")] + { + let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; + let email_contents = email_types::InviteUser { + recipient_email: invitee_email, + user_name: domain::UserName::new(new_user.get_name())?, + settings: state.conf.clone(), + subject: "You have been invited to join Hyperswitch Community!", + merchant_id: user_from_token.merchant_id.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); + is_email_sent = send_email_result.is_ok(); + } + #[cfg(not(feature = "email"))] + { + is_email_sent = false; + } + + Ok(InviteMultipleUserResponse { + is_email_sent, + password: if cfg!(not(feature = "email")) { + Some(new_user.get_password().get_secret()) + } else { + None + }, + email: request.email.clone(), + error: None, + }) +} + +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 user_from_token.merchant_id == request.merchant_id { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User switch to same merchant id."); + } + + let user_roles = state + .store + .list_user_roles_by_user_id(&user_from_token.user_id) + .await + .change_context(UserErrors::InternalServerError)?; + + let active_user_roles = user_roles + .into_iter() + .filter(|role| role.status == UserStatus::Active) + .collect::>(); + + let user = user_from_token.get_user(state.clone()).await?.into(); + + let (token, role_id) = if utils::user_role::is_internal_role(&user_from_token.role_id) { + 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 token = utils::user::generate_jwt_auth_token_with_custom_role_attributes( + state, + &user, + request.merchant_id.clone(), + org_id, + user_from_token.role_id.clone(), + ) + .await?; + (token, user_from_token.role_id) + } else { + let user_role = active_user_roles + .iter() + .find(|role| role.merchant_id == request.merchant_id) + .ok_or(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User doesn't have access to switch")?; + + let token = utils::user::generate_jwt_auth_token(&state, &user, user_role).await?; + (token, user_role.role_id.clone()) + }; + + Ok(ApplicationResponse::Json( + user_api::SwitchMerchantResponse { + token, + name: user.get_name(), + email: user.get_email(), + user_id: user.get_user_id().to_string(), + verification_days_left: None, + user_role: role_id, + merchant_id: request.merchant_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) +} + +pub async fn list_merchant_ids_for_user( + state: AppState, + user: auth::UserFromToken, +) -> UserResponse> { + let user_roles = + utils::user_role::get_active_user_roles_for_user(&state, &user.user_id).await?; + + let merchant_accounts = state + .store + .list_multiple_merchant_accounts( + user_roles + .iter() + .map(|role| role.merchant_id.clone()) + .collect(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + Ok(ApplicationResponse::Json( + utils::user::get_multiple_merchant_details_with_status(user_roles, merchant_accounts)?, + )) +} + +pub async fn get_users_for_merchant_account( + state: AppState, + user_from_token: auth::UserFromToken, +) -> UserResponse { + let users = state + .store + .find_users_and_roles_by_merchant_id(user_from_token.merchant_id.as_str()) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("No users for given merchant id")? + .into_iter() + .filter_map(|(user, role)| domain::UserAndRoleJoined(user, role).try_into().ok()) + .collect(); + + Ok(ApplicationResponse::Json(user_api::GetUsersResponse(users))) +} + +#[cfg(feature = "email")] +pub async fn verify_email_without_invite_checks( + state: AppState, + req: user_api::VerifyEmailRequest, +) -> UserResponse { + let token = auth::decode_jwt::(&req.token.clone().expose(), &state) + .await + .change_context(UserErrors::LinkInvalid)?; + let user = state + .store + .find_user_by_email(token.get_email()) + .await + .change_context(UserErrors::InternalServerError)?; + let user = state + .store + .update_user_by_user_id(user.user_id.as_str(), storage_user::UserUpdate::VerifyUser) + .await + .change_context(UserErrors::InternalServerError)?; + let user_from_db: domain::UserFromStorage = user.into(); + let user_role = user_from_db.get_role_from_db(state.clone()).await?; + let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, + )) +} + +#[cfg(feature = "email")] +pub async fn verify_email( + state: AppState, + req: user_api::VerifyEmailRequest, +) -> UserResponse { + let token = auth::decode_jwt::(&req.token.clone().expose(), &state) + .await + .change_context(UserErrors::LinkInvalid)?; + + let user = state + .store + .find_user_by_email(token.get_email()) + .await + .change_context(UserErrors::InternalServerError)?; + + let user = state + .store + .update_user_by_user_id(user.user_id.as_str(), storage_user::UserUpdate::VerifyUser) + .await + .change_context(UserErrors::InternalServerError)?; + + let user_from_db: domain::UserFromStorage = user.into(); + + let signin_strategy = + if let Some(preferred_merchant_id) = user_from_db.get_preferred_merchant_id() { + let preferred_role = user_from_db + .get_role_from_db_by_merchant_id(&state, preferred_merchant_id.as_str()) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("User role with preferred_merchant_id not found")?; + domain::SignInWithRoleStrategyType::SingleRole(domain::SignInWithSingleRoleStrategy { + user: user_from_db, + user_role: preferred_role, + }) + } else { + let user_roles = user_from_db.get_roles_from_db(&state).await?; + domain::SignInWithRoleStrategyType::decide_signin_strategy_by_user_roles( + user_from_db, + user_roles, + ) + .await? + }; + + Ok(ApplicationResponse::Json( + signin_strategy.get_signin_response(&state).await?, + )) +} + +#[cfg(feature = "email")] +pub async fn send_verification_mail( + state: AppState, + req: user_api::SendVerifyEmailRequest, +) -> UserResponse<()> { + let user_email = domain::UserEmail::try_from(req.email)?; + let user = state + .store + .find_user_by_email(user_email.clone().get_secret().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::UserNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + if user.is_verified { + return Err(UserErrors::UserAlreadyVerified.into()); + } + + let email_contents = email_types::VerifyEmail { + recipient_email: domain::UserEmail::from_pii_email(user.email)?, + settings: state.conf.clone(), + subject: "Welcome to the Hyperswitch community!", + }; + + state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + Ok(ApplicationResponse::StatusOk) +} + +#[cfg(feature = "recon")] +pub async fn verify_token( + state: AppState, + req: auth::ReconUser, +) -> UserResponse { + let user = state + .store + .find_user_by_id(&req.user_id) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::UserNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + let merchant_id = state + .store + .find_user_role_by_user_id(&req.user_id) + .await + .change_context(UserErrors::InternalServerError)? + .merchant_id; + + Ok(ApplicationResponse::Json(user_api::VerifyTokenResponse { + merchant_id: merchant_id.to_string(), + user_email: user.email, + })) +} + +pub async fn update_user_details( + state: AppState, + user_token: auth::UserFromToken, + req: user_api::UpdateUserAccountDetailsRequest, +) -> UserResponse<()> { + let user: domain::UserFromStorage = state + .store + .find_user_by_id(&user_token.user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + let name = req.name.map(domain::UserName::new).transpose()?; + + if let Some(ref preferred_merchant_id) = req.preferred_merchant_id { + let _ = state + .store + .find_user_role_by_user_id_merchant_id(user.get_user_id(), preferred_merchant_id) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + } + + let user_update = storage_user::UserUpdate::AccountUpdate { + name: name.map(|x| x.get_secret().expose()), + password: None, + is_verified: None, + preferred_merchant_id: req.preferred_merchant_id, + }; + + state + .store + .update_user_by_user_id(user.get_user_id(), user_update) + .await + .change_context(UserErrors::InternalServerError)?; + + 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..24ff292870e5 --- /dev/null +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -0,0 +1,679 @@ +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; +#[cfg(feature = "email")] +use router_env::logger; + +#[cfg(feature = "email")] +use crate::services::email::types as email_types; +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::ConfigurationType(req) => { + Ok(types::MetaData::ConfigurationType(req)) + } + api::SetMetaDataRequest::IntegrationCompleted => { + Ok(types::MetaData::IntegrationCompleted(true)) + } + api::SetMetaDataRequest::SPRoutingConfigured(req) => { + Ok(types::MetaData::SPRoutingConfigured(req)) + } + api::SetMetaDataRequest::Feedback(req) => Ok(types::MetaData::Feedback(req)), + api::SetMetaDataRequest::ProdIntent(req) => Ok(types::MetaData::ProdIntent(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::ConfigurationType => DBEnum::ConfigurationType, + api::GetMetaDataRequest::IntegrationCompleted => DBEnum::IntegrationCompleted, + api::GetMetaDataRequest::StripeConnected => DBEnum::StripeConnected, + api::GetMetaDataRequest::PaypalConnected => DBEnum::PaypalConnected, + api::GetMetaDataRequest::SPRoutingConfigured => DBEnum::SpRoutingConfigured, + api::GetMetaDataRequest::Feedback => DBEnum::Feedback, + api::GetMetaDataRequest::ProdIntent => DBEnum::ProdIntent, + 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::ConfigurationType => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::ConfigurationType(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::Feedback => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::Feedback(resp)) + } + DBEnum::ProdIntent => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::ProdIntent(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) => { + let mut metadata = utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), + metadata_key, + data.clone(), + ) + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_merchant_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata + } + types::MetaData::ConfigurationType(data) => { + let mut metadata = utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), + metadata_key, + data.clone(), + ) + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_merchant_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata + } + 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::Feedback(data) => { + let mut metadata = utils::insert_user_scoped_metadata_to_db( + state, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), + metadata_key, + data.clone(), + ) + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_user_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata + } + types::MetaData::ProdIntent(data) => { + let mut metadata = utils::insert_user_scoped_metadata_to_db( + state, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), + metadata_key, + data.clone(), + ) + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_user_scoped_metadata( + state, + user.user_id.clone(), + user.merchant_id, + user.org_id, + metadata_key, + data.clone(), + ) + .await + .change_context(UserErrors::InternalServerError); + } + + #[cfg(feature = "email")] + { + if utils::is_prod_email_required(&data) { + let email_contents = email_types::BizEmailProd::new(state, data)?; + 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); + } + } + + metadata + } + 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, user_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); + } + + if !user_scoped_enums.is_empty() { + let mut res = utils::get_user_scoped_metadata_from_db( + state, + user.user_id.to_owned(), + user.merchant_id.to_owned(), + user.org_id.to_owned(), + user_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/sample_data.rs b/crates/router/src/core/user/sample_data.rs new file mode 100644 index 000000000000..19b7d3bd815c --- /dev/null +++ b/crates/router/src/core/user/sample_data.rs @@ -0,0 +1,82 @@ +use api_models::user::sample_data::SampleDataRequest; +use common_utils::errors::ReportSwitchExt; +use data_models::payments::payment_intent::PaymentIntentNew; +use diesel_models::{user::sample_data::PaymentAttemptBatchNew, RefundNew}; + +pub type SampleDataApiResponse = SampleDataResult>; + +use crate::{ + core::errors::sample_data::SampleDataResult, + routes::AppState, + services::{authentication::UserFromToken, ApplicationResponse}, + utils::user::sample_data::generate_sample_data, +}; + +pub async fn generate_sample_data_for_user( + state: AppState, + user_from_token: UserFromToken, + req: SampleDataRequest, +) -> SampleDataApiResponse<()> { + let sample_data = + generate_sample_data(&state, req, user_from_token.merchant_id.as_str()).await?; + + let (payment_intents, payment_attempts, refunds): ( + Vec, + Vec, + Vec, + ) = sample_data.into_iter().fold( + (Vec::new(), Vec::new(), Vec::new()), + |(mut pi, mut pa, mut rf), (payment_intent, payment_attempt, refund)| { + pi.push(payment_intent); + pa.push(payment_attempt); + if let Some(refund) = refund { + rf.push(refund); + } + (pi, pa, rf) + }, + ); + + state + .store + .insert_payment_intents_batch_for_sample_data(payment_intents) + .await + .switch()?; + state + .store + .insert_payment_attempts_batch_for_sample_data(payment_attempts) + .await + .switch()?; + state + .store + .insert_refunds_batch_for_sample_data(refunds) + .await + .switch()?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn delete_sample_data_for_user( + state: AppState, + user_from_token: UserFromToken, + _req: SampleDataRequest, +) -> SampleDataApiResponse<()> { + let merchant_id_del = user_from_token.merchant_id.as_str(); + + state + .store + .delete_payment_intents_for_sample_data(merchant_id_del) + .await + .switch()?; + state + .store + .delete_payment_attempts_for_sample_data(merchant_id_del) + .await + .switch()?; + state + .store + .delete_refunds_for_sample_data(merchant_id_del) + .await + .switch()?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs new file mode 100644 index 000000000000..742c281b89ad --- /dev/null +++ b/crates/router/src/core/user_role.rs @@ -0,0 +1,250 @@ +use api_models::user_role as user_role_api; +use diesel_models::{enums::UserStatus, user_role::UserRoleUpdate}; +use error_stack::ResultExt; +use masking::ExposeInterface; +use router_env::logger; + +use crate::{ + core::errors::{UserErrors, UserResponse}, + routes::AppState, + services::{ + authentication::{self as auth}, + authorization::{info, predefined_permissions}, + ApplicationResponse, + }, + types::domain, + utils, +}; + +pub async fn get_authorization_info( + _state: AppState, +) -> UserResponse { + Ok(ApplicationResponse::Json( + user_role_api::AuthorizationInfoResponse( + info::get_authorization_info() + .into_iter() + .map(Into::into) + .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 get_role_from_token( + _state: AppState, + user: auth::UserFromToken, +) -> UserResponse> { + Ok(ApplicationResponse::Json( + predefined_permissions::PREDEFINED_PERMISSIONS + .get(user.role_id.as_str()) + .ok_or(UserErrors::InternalServerError.into()) + .attach_printable("Invalid Role Id in JWT")? + .get_permissions() + .iter() + .map(|&per| per.into()) + .collect(), + )) +} + +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) +} + +pub async fn accept_invitation( + state: AppState, + user_token: auth::UserWithoutMerchantFromToken, + req: user_role_api::AcceptInvitationRequest, +) -> UserResponse { + let user_role = futures::future::join_all(req.merchant_ids.iter().map(|merchant_id| async { + state + .store + .update_user_role_by_user_id_merchant_id( + user_token.user_id.as_str(), + merchant_id, + UserRoleUpdate::UpdateStatus { + status: UserStatus::Active, + modified_by: user_token.user_id.clone(), + }, + ) + .await + .map_err(|e| { + logger::error!("Error while accepting invitation {}", e); + }) + .ok() + })) + .await + .into_iter() + .reduce(Option::or) + .flatten() + .ok_or(UserErrors::MerchantIdNotFound)?; + + if let Some(true) = req.need_dashboard_entry_response { + let user_from_db = state + .store + .find_user_by_id(user_token.user_id.as_str()) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + return Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, + )); + } + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn delete_user_role( + state: AppState, + user_from_token: auth::UserFromToken, + request: user_role_api::DeleteUserRoleRequest, +) -> UserResponse<()> { + let user_from_db: domain::UserFromStorage = state + .store + .find_user_by_email( + domain::UserEmail::from_pii_email(request.email)? + .get_secret() + .expose() + .as_str(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::InvalidRoleOperation) + .attach_printable("User not found in records") + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .into(); + + if user_from_db.get_user_id() == user_from_token.user_id { + return Err(UserErrors::InvalidDeleteOperation.into()) + .attach_printable("User deleting himself"); + } + + let user_roles = state + .store + .list_user_roles_by_user_id(user_from_db.get_user_id()) + .await + .change_context(UserErrors::InternalServerError)?; + + match user_roles + .iter() + .find(|&role| role.merchant_id == user_from_token.merchant_id.as_str()) + { + Some(user_role) => { + if !predefined_permissions::is_role_deletable(&user_role.role_id) { + return Err(UserErrors::InvalidRoleId.into()) + .attach_printable("Deletion not allowed for users with specific role id"); + } + } + None => { + return Err(UserErrors::InvalidDeleteOperation.into()) + .attach_printable("User is not associated with the merchant"); + } + }; + + if user_roles.len() > 1 { + state + .store + .delete_user_role_by_user_id_merchant_id( + user_from_db.get_user_id(), + user_from_token.merchant_id.as_str(), + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error while deleting user role")?; + + Ok(ApplicationResponse::StatusOk) + } else { + state + .store + .delete_user_by_user_id(user_from_db.get_user_id()) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error while deleting user entry")?; + + state + .store + .delete_user_role_by_user_id_merchant_id( + user_from_db.get_user_id(), + user_from_token.merchant_id.as_str(), + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error while deleting user role")?; + + Ok(ApplicationResponse::StatusOk) + } +} diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index fb3dc3e7d281..ef8dddde9554 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,18 +1,11 @@ use std::{marker::PhantomData, str::FromStr}; -use api_models::{ - enums::{DisputeStage, DisputeStatus}, - payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, -}; +use api_models::enums::{DisputeStage, DisputeStatus}; +use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; -use common_utils::{ - errors::CustomResult, - ext_traits::{AsyncExt, Encode}, -}; +use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; use error_stack::{report, IntoReport, ResultExt}; -use euclid::enums as euclid_enums; -use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; use uuid::Uuid; @@ -48,33 +41,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 +70,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 +116,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()) @@ -194,6 +175,9 @@ pub async fn construct_payout_router_data<'a, F>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) @@ -345,6 +329,9 @@ pub async fn construct_refund_router_data<'a, F>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, + refund_id: Some(refund.refund_id.clone()), + dispute_id: None, }; Ok(router_data) @@ -574,6 +561,9 @@ pub async fn construct_accept_dispute_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, + dispute_id: Some(dispute.dispute_id.clone()), + refund_id: None, }; Ok(router_data) } @@ -661,6 +651,9 @@ pub async fn construct_submit_evidence_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: Some(dispute.dispute_id.clone()), }; Ok(router_data) } @@ -754,6 +747,9 @@ pub async fn construct_upload_file_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) } @@ -844,6 +840,9 @@ pub async fn construct_defend_dispute_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: Some(dispute.dispute_id.clone()), }; Ok(router_data) } @@ -927,6 +926,9 @@ pub async fn construct_retrieve_file_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) } @@ -1082,64 +1084,33 @@ pub fn get_flow_name() -> RouterResult { .to_string()) } -pub async fn persist_individual_surcharge_details_in_redis( - state: &AppState, - merchant_account: &domain::MerchantAccount, - surcharge_metadata: &SurchargeMetadata, -) -> RouterResult<()> { - if !surcharge_metadata.is_empty_result() { - let redis_conn = state - .store - .get_redis_conn() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get redis connection")?; - let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key( - &surcharge_metadata.payment_attempt_id, - ); - - let mut value_list = Vec::with_capacity(surcharge_metadata.get_surcharge_results_size()); - for (key, value) in surcharge_metadata - .get_individual_surcharge_key_value_pairs() - .into_iter() - { - value_list.push(( - key, - Encode::::encode_to_string_of_json(&value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode to string of json")?, - )); - } - let intent_fulfillment_time = merchant_account - .intent_fulfillment_time - .unwrap_or(consts::DEFAULT_FULFILLMENT_TIME); - redis_conn - .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to write to redis")?; - } - Ok(()) +pub fn get_request_incremental_authorization_value( + request_incremental_authorization: Option, + capture_method: Option, +) -> RouterResult> { + Some(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()))).transpose() } -pub async fn get_individual_surcharge_detail_from_redis( - state: &AppState, - payment_method: &euclid_enums::PaymentMethod, - payment_method_type: &euclid_enums::PaymentMethodType, - card_network: Option, - payment_attempt_id: &str, -) -> CustomResult { - let redis_conn = state - .store - .get_redis_conn() - .attach_printable("Failed to get redis connection")?; - let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); - let value_key = SurchargeMetadata::get_surcharge_details_redis_hashset_key( - payment_method, - payment_method_type, - card_network.as_ref(), - ); - - redis_conn - .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetailsResponse") - .await +pub fn get_incremental_authorization_allowed_value( + incremental_authorization_allowed: Option, + request_incremental_authorization: Option, +) -> Option { + if request_incremental_authorization + == Some(common_enums::RequestIncrementalAuthorization::False) + { + Some(false) + } else { + incremental_authorization_allowed + } } diff --git a/crates/router/src/core/verification.rs b/crates/router/src/core/verification.rs index e643e0455b8b..bac47b34dced 100644 --- a/crates/router/src/core/verification.rs +++ b/crates/router/src/core/verification.rs @@ -1,16 +1,11 @@ pub mod utils; use api_models::verifications::{self, ApplepayMerchantResponse}; -use common_utils::{errors::CustomResult, ext_traits::Encode}; +use common_utils::{errors::CustomResult, request::RequestContent}; use error_stack::ResultExt; #[cfg(feature = "kms")] use external_services::kms; -use crate::{ - core::errors::{self, api_error_response}, - headers, logger, - routes::AppState, - services, types, -}; +use crate::{core::errors::api_error_response, headers, logger, routes::AppState, services}; const APPLEPAY_INTERNAL_MERCHANT_NAME: &str = "Applepay_merchant"; @@ -57,13 +52,6 @@ pub async fn verify_merchant_creds_for_applepay( partner_merchant_name: APPLEPAY_INTERNAL_MERCHANT_NAME.to_string(), }; - let applepay_req = types::RequestBody::log_and_get_request_body( - &request_body, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode ApplePay session request to a string of json")?; - let apple_pay_merch_verification_req = services::RequestBuilder::new() .method(services::Method::Post) .url(applepay_endpoint) @@ -72,7 +60,7 @@ pub async fn verify_merchant_creds_for_applepay( headers::CONTENT_TYPE.to_string(), "application/json".to_string().into(), )]) - .body(Some(applepay_req)) + .set_body(RequestContent::Json(Box::new(request_body))) .add_certificate(Some(cert_data)) .add_certificate_key(Some(key_data)) .build(); 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 db53a3b56a15..f0348e45eb30 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, request::RequestContent}; 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,17 +25,20 @@ use crate::{ payments, refunds, }, db::StorageInterface, + events::{ + api_logs::ApiEvent, + outgoing_webhook_logs::{OutgoingWebhookEvent, OutgoingWebhookEventMetric}, + }, 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}, domain, storage::{self, enums}, transformers::{ForeignInto, ForeignTryInto}, }, - utils::{self as helper_utils, generate_id, Encode, OptionExt, ValueExt}, + utils::{self as helper_utils, generate_id, OptionExt, ValueExt}, }; const OUTGOING_WEBHOOK_TIMEOUT_SECS: u64 = 5; @@ -417,6 +421,7 @@ pub async fn mandates_incoming_webhook_flow( state: AppState, merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, + key_store: domain::MerchantKeyStore, webhook_details: api::IncomingWebhookDetails, source_verified: bool, event_type: api_models::webhooks::IncomingWebhookEvent, @@ -460,8 +465,12 @@ pub async fn mandates_incoming_webhook_flow( .await .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; let mandates_response = Box::new( - api::mandates::MandateResponse::from_db_mandate(&state, updated_mandate.clone()) - .await?, + api::mandates::MandateResponse::from_db_mandate( + &state, + key_store, + updated_mandate.clone(), + ) + .await?, ); let event_type: Option = updated_mandate.mandate_status.foreign_into(); if let Some(outgoing_event_type) = event_type { @@ -594,7 +603,7 @@ async fn bank_transfer_webhook_flow(business_profile, outgoing_webhook, &state).await; + trigger_webhook_to_merchant::(business_profile, outgoing_webhook, state).await; if let Err(e) = result { + error.replace( + serde_json::to_value(e.current_context()) + .into_report() + .attach_printable("Failed to serialize json error response") + .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) + .ok() + .into(), + ); logger::error!(?e); } + let outgoing_webhook_event_type = content.get_outgoing_webhook_event_type(); + let webhook_event = OutgoingWebhookEvent::new( + merchant_account.merchant_id.clone(), + event.event_id.clone(), + event_type, + outgoing_webhook_event_type, + error.is_some(), + error, + ); + match webhook_event.clone().try_into() { + Ok(event) => { + state_clone.event_handler().log_event(event); + } + Err(err) => { + logger::error!(error=?err, event=?webhook_event, "Error Logging Outgoing Webhook Event"); + } + } }); } @@ -754,7 +789,7 @@ pub async fn create_event_and_trigger_outgoing_webhook( business_profile: diesel_models::business_profile::BusinessProfile, webhook: api::OutgoingWebhook, - state: &AppState, + state: AppState, ) -> CustomResult<(), errors::WebhooksFlowError> { let webhook_details_json = business_profile .webhook_details @@ -779,13 +814,6 @@ pub async fn trigger_webhook_to_merchant( let outgoing_webhooks_signature = transformed_outgoing_webhook .get_outgoing_webhooks_signature(business_profile.payment_response_hash_key.clone())?; - let transformed_outgoing_webhook_string = router_types::RequestBody::log_and_get_request_body( - &transformed_outgoing_webhook, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::WebhooksFlowError::OutgoingWebhookEncodingFailed) - .attach_printable("There was an issue when encoding the outgoing webhook body")?; - let mut header = vec![( reqwest::header::CONTENT_TYPE.to_string(), "application/json".into(), @@ -800,12 +828,12 @@ pub async fn trigger_webhook_to_merchant( .url(&webhook_url) .attach_default_headers() .headers(header) - .body(Some(transformed_outgoing_webhook_string)) + .set_body(RequestContent::Json(Box::new(transformed_outgoing_webhook))) .build(); let response = state .api_client - .send_request(state, request, Some(OUTGOING_WEBHOOK_TIMEOUT_SECS), false) + .send_request(&state, request, Some(OUTGOING_WEBHOOK_TIMEOUT_SECS), false) .await; metrics::WEBHOOK_OUTGOING_COUNT.add( @@ -860,6 +888,7 @@ pub async fn trigger_webhook_to_merchant( } pub async fn webhooks_wrapper( + flow: &impl router_env::types::FlowMetric, state: AppState, req: &actix_web::HttpRequest, merchant_account: domain::MerchantAccount, @@ -867,21 +896,67 @@ pub async fn webhooks_wrapper RouterResponse { - let (application_response, _webhooks_response_tracker) = Box::pin(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, + req.method(), + ); + 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, @@ -892,6 +967,7 @@ pub async fn webhooks_core errors::RouterResult<( services::ApplicationResponse, WebhookResponseTracker, + serde_json::Value, )> { metrics::WEBHOOK_INCOMING_COUNT.add( &metrics::CONTEXT, @@ -973,7 +1049,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) { @@ -1072,14 +1153,19 @@ pub async fn webhooks_core::encode_to_vec(&event_object) + resource_object: serde_json::to_vec(&event_object) + .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", @@ -1156,6 +1242,7 @@ pub async fn webhooks_core + Sync + Send + std::fmt::Debug + Serialize + From + Sync + Send + std::fmt::Debug + 'static { fn get_outgoing_webhooks_signature( &self, diff --git a/crates/router/src/core/webhooks/utils.rs b/crates/router/src/core/webhooks/utils.rs index 322440e53138..75d37f942798 100644 --- a/crates/router/src/core/webhooks/utils.rs +++ b/crates/router/src/core/webhooks/utils.rs @@ -113,6 +113,9 @@ pub async fn construct_webhook_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) } diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 9687f7f97c92..549001772464 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -1,17 +1,24 @@ pub mod address; pub mod api_keys; +pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; +pub mod blocklist_lookup; pub mod business_profile; pub mod cache; pub mod capture; pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod ephemeral_key; pub mod events; pub mod file; pub mod fraud_check; pub mod gsm; +pub mod health_check; +mod kafka_store; pub mod locker_mock_up; pub mod mandate; pub mod merchant_account; @@ -31,11 +38,24 @@ pub mod user_role; use data_models::payments::{ payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, }; +use diesel_models::{ + fraud_check::{FraudCheck, FraudCheckNew, FraudCheckUpdate}, + organization::{Organization, OrganizationNew, OrganizationUpdate}, +}; +use error_stack::ResultExt; use masking::PeekInterface; use redis_interface::errors::RedisError; -use storage_impl::{redis::kv_store::RedisConnInterface, MockDb}; +use storage_impl::{errors::StorageError, redis::kv_store::RedisConnInterface, MockDb}; -use crate::{errors::CustomResult, services::Store}; +pub use self::kafka_store::KafkaStore; +use self::{fraud_check::FraudCheckInterface, organization::OrganizationInterface}; +pub use crate::{ + errors::CustomResult, + services::{ + kafka::{KafkaError, KafkaProducer, MQResult}, + Store, + }, +}; #[derive(PartialEq, Eq)] pub enum StorageImpl { @@ -51,14 +71,16 @@ pub trait StorageInterface: + dyn_clone::DynClone + address::AddressInterface + api_keys::ApiKeyInterface + + blocklist_lookup::BlocklistLookupInterface + configs::ConfigInterface + capture::CaptureInterface + customers::CustomerInterface + + dashboard_metadata::DashboardMetadataInterface + dispute::DisputeInterface + ephemeral_key::EphemeralKeyInterface + events::EventInterface + file::FileMetadataInterface - + fraud_check::FraudCheckInterface + + FraudCheckInterface + locker_mock_up::LockerMockUpInterface + mandate::MandateInterface + merchant_account::MerchantAccountInterface @@ -67,6 +89,8 @@ pub trait StorageInterface: + PaymentAttemptInterface + PaymentIntentInterface + payment_method::PaymentMethodInterface + + blocklist::BlocklistInterface + + blocklist_fingerprint::BlocklistFingerprintInterface + scheduler::SchedulerInterface + payout_attempt::PayoutAttemptInterface + payouts::PayoutsInterface @@ -79,11 +103,14 @@ pub trait StorageInterface: + RedisConnInterface + RequestIdStore + business_profile::BusinessProfileInterface - + organization::OrganizationInterface + + OrganizationInterface + routing_algorithm::RoutingAlgorithmInterface + gsm::GsmInterface + user::UserInterface + user_role::UserRoleInterface + + authorization::AuthorizationInterface + + user::sample_data::BatchSampleDataInterface + + health_check::HealthCheckDbInterface + 'static { fn get_scheduler_db(&self) -> Box; @@ -151,7 +178,6 @@ where T: serde::de::DeserializeOwned, { use common_utils::ext_traits::ByteSliceExt; - use error_stack::ResultExt; let bytes = db.get_key(key).await?; bytes @@ -160,3 +186,72 @@ where } dyn_clone::clone_trait_object!(StorageInterface); + +impl RequestIdStore for KafkaStore { + fn add_request_id(&mut self, request_id: String) { + self.diesel_store.add_request_id(request_id) + } +} + +#[async_trait::async_trait] +impl FraudCheckInterface for KafkaStore { + async fn insert_fraud_check_response( + &self, + new: FraudCheckNew, + ) -> 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/api_keys.rs b/crates/router/src/db/api_keys.rs index 4ba9e47e9a5d..94edac969026 100644 --- a/crates/router/src/db/api_keys.rs +++ b/crates/router/src/db/api_keys.rs @@ -535,6 +535,7 @@ mod tests { merchant_id, hashed_api_key.into_inner() ),) + .await .is_none() ) } diff --git a/crates/router/src/db/authorization.rs b/crates/router/src/db/authorization.rs new file mode 100644 index 000000000000..d167d1775375 --- /dev/null +++ b/crates/router/src/db/authorization.rs @@ -0,0 +1,148 @@ +use diesel_models::authorization::AuthorizationUpdateInternal; +use error_stack::IntoReport; + +use super::{MockDb, Store}; +use crate::{ + connection, + core::errors::{self, CustomResult}, + types::storage, +}; + +#[async_trait::async_trait] +pub trait AuthorizationInterface { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult; + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError>; + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl AuthorizationInterface for Store { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + authorization + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::Authorization::find_by_merchant_id_payment_id(&conn, merchant_id, payment_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Authorization::update_by_merchant_id_authorization_id( + &conn, + merchant_id, + authorization_id, + authorization, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl AuthorizationInterface for MockDb { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult { + let mut authorizations = self.authorizations.lock().await; + if authorizations.iter().any(|authorization_inner| { + authorization_inner.authorization_id == authorization.authorization_id + }) { + Err(errors::StorageError::DuplicateValue { + entity: "authorization_id", + key: None, + })? + } + let authorization = storage::Authorization { + authorization_id: authorization.authorization_id, + merchant_id: authorization.merchant_id, + payment_id: authorization.payment_id, + amount: authorization.amount, + created_at: common_utils::date_time::now(), + modified_at: common_utils::date_time::now(), + status: authorization.status, + error_code: authorization.error_code, + error_message: authorization.error_message, + connector_authorization_id: authorization.connector_authorization_id, + previously_authorized_amount: authorization.previously_authorized_amount, + }; + authorizations.push(authorization.clone()); + Ok(authorization) + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + let authorizations = self.authorizations.lock().await; + let authorizations_found: Vec = authorizations + .iter() + .filter(|a| a.merchant_id == merchant_id && a.payment_id == payment_id) + .cloned() + .collect(); + + Ok(authorizations_found) + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization_update: storage::AuthorizationUpdate, + ) -> CustomResult { + let mut authorizations = self.authorizations.lock().await; + authorizations + .iter_mut() + .find(|authorization| authorization.authorization_id == authorization_id && authorization.merchant_id == merchant_id) + .map(|authorization| { + let authorization_updated = + AuthorizationUpdateInternal::from(authorization_update) + .create_authorization(authorization.clone()); + *authorization = authorization_updated.clone(); + authorization_updated + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "cannot find authorization for authorization_id = {authorization_id} and merchant_id = {merchant_id}" + )) + .into(), + ) + } +} diff --git a/crates/router/src/db/blocklist.rs b/crates/router/src/db/blocklist.rs new file mode 100644 index 000000000000..93361552de70 --- /dev/null +++ b/crates/router/src/db/blocklist.rs @@ -0,0 +1,211 @@ +use error_stack::IntoReport; +use router_env::{instrument, tracing}; +use storage_impl::MockDb; + +use super::Store; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + types::storage, +}; + +#[async_trait::async_trait] +pub trait BlocklistInterface { + async fn insert_blocklist_entry( + &self, + pm_blocklist_new: storage::BlocklistNew, + ) -> CustomResult; + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult; + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult; + + async fn list_blocklist_entries_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError>; + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError>; +} + +#[async_trait::async_trait] +impl BlocklistInterface for Store { + #[instrument(skip_all)] + async fn insert_blocklist_entry( + &self, + pm_blocklist: storage::BlocklistNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + pm_blocklist + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::find_by_merchant_id_fingerprint_id(&conn, merchant_id, fingerprint_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_blocklist_entries_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::list_by_merchant_id(&conn, merchant_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::list_by_merchant_id_data_kind( + &conn, + merchant_id, + data_kind, + limit, + offset, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::delete_by_merchant_id_fingerprint_id(&conn, merchant_id, fingerprint_id) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl BlocklistInterface for MockDb { + #[instrument(skip_all)] + async fn insert_blocklist_entry( + &self, + _pm_blocklist: storage::BlocklistNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn list_blocklist_entries_by_merchant_id( + &self, + _merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + _merchant_id: &str, + _data_kind: common_enums::BlocklistDataKind, + _limit: i64, + _offset: i64, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl BlocklistInterface for KafkaStore { + #[instrument(skip_all)] + async fn insert_blocklist_entry( + &self, + pm_blocklist: storage::BlocklistNew, + ) -> CustomResult { + self.diesel_store.insert_blocklist_entry(pm_blocklist).await + } + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + self.diesel_store + .find_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, fingerprint) + .await + } + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + self.diesel_store + .delete_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, fingerprint) + .await + } + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_blocklist_entries_by_merchant_id_data_kind(merchant_id, data_kind, limit, offset) + .await + } + + async fn list_blocklist_entries_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_blocklist_entries_by_merchant_id(merchant_id) + .await + } +} diff --git a/crates/router/src/db/blocklist_fingerprint.rs b/crates/router/src/db/blocklist_fingerprint.rs new file mode 100644 index 000000000000..d9107d3d1c13 --- /dev/null +++ b/crates/router/src/db/blocklist_fingerprint.rs @@ -0,0 +1,99 @@ +use error_stack::IntoReport; +use router_env::{instrument, tracing}; +use storage_impl::MockDb; + +use super::Store; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + types::storage, +}; + +#[async_trait::async_trait] +pub trait BlocklistFingerprintInterface { + async fn insert_blocklist_fingerprint_entry( + &self, + pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult; + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl BlocklistFingerprintInterface for Store { + #[instrument(skip_all)] + async fn insert_blocklist_fingerprint_entry( + &self, + pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + pm_fingerprint_new + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::BlocklistFingerprint::find_by_merchant_id_fingerprint_id( + &conn, + merchant_id, + fingerprint_id, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl BlocklistFingerprintInterface for MockDb { + #[instrument(skip_all)] + async fn insert_blocklist_fingerprint_entry( + &self, + _pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl BlocklistFingerprintInterface for KafkaStore { + #[instrument(skip_all)] + async fn insert_blocklist_fingerprint_entry( + &self, + pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult { + self.diesel_store + .insert_blocklist_fingerprint_entry(pm_fingerprint_new) + .await + } + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + self.diesel_store + .find_blocklist_fingerprint_by_merchant_id_fingerprint_id(merchant_id, fingerprint) + .await + } +} diff --git a/crates/router/src/db/blocklist_lookup.rs b/crates/router/src/db/blocklist_lookup.rs new file mode 100644 index 000000000000..f5fb4ea9ed8c --- /dev/null +++ b/crates/router/src/db/blocklist_lookup.rs @@ -0,0 +1,131 @@ +use error_stack::IntoReport; +use router_env::{instrument, tracing}; +use storage_impl::MockDb; + +use super::Store; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + types::storage, +}; + +#[async_trait::async_trait] +pub trait BlocklistLookupInterface { + async fn insert_blocklist_lookup_entry( + &self, + blocklist_lookup_new: storage::BlocklistLookupNew, + ) -> CustomResult; + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult; + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl BlocklistLookupInterface for Store { + #[instrument(skip_all)] + async fn insert_blocklist_lookup_entry( + &self, + blocklist_lookup_entry: storage::BlocklistLookupNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + blocklist_lookup_entry + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::BlocklistLookup::find_by_merchant_id_fingerprint(&conn, merchant_id, fingerprint) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::BlocklistLookup::delete_by_merchant_id_fingerprint(&conn, merchant_id, fingerprint) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl BlocklistLookupInterface for MockDb { + #[instrument(skip_all)] + async fn insert_blocklist_lookup_entry( + &self, + _blocklist_lookup_entry: storage::BlocklistLookupNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + _merchant_id: &str, + _fingerprint: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + _merchant_id: &str, + _fingerprint: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl BlocklistLookupInterface for KafkaStore { + #[instrument(skip_all)] + async fn insert_blocklist_lookup_entry( + &self, + blocklist_lookup_entry: storage::BlocklistLookupNew, + ) -> CustomResult { + self.diesel_store + .insert_blocklist_lookup_entry(blocklist_lookup_entry) + .await + } + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + self.diesel_store + .find_blocklist_lookup_entry_by_merchant_id_fingerprint(merchant_id, fingerprint) + .await + } + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + self.diesel_store + .delete_blocklist_lookup_entry_by_merchant_id_fingerprint(merchant_id, fingerprint) + .await + } +} diff --git a/crates/router/src/db/cache.rs b/crates/router/src/db/cache.rs index 0688665f0c4c..4bda08e22c03 100644 --- a/crates/router/src/db/cache.rs +++ b/crates/router/src/db/cache.rs @@ -63,7 +63,7 @@ where F: FnOnce() -> Fut + Send, Fut: futures::Future> + Send, { - let cache_val = cache.get_val::(key); + let cache_val = cache.get_val::(key).await; if let Some(val) = cache_val { Ok(val) } else { diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs new file mode 100644 index 000000000000..8e2ac0b6ad3f --- /dev/null +++ b/crates/router/src/db/dashboard_metadata.rs @@ -0,0 +1,297 @@ +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 update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> 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 fn delete_user_scoped_dashboard_metadata_by_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult; +} + +#[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 update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::update( + &conn, + user_id, + merchant_id, + org_id, + data_key, + dashboard_metadata_update, + ) + .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 fn delete_user_scoped_dashboard_metadata_by_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::delete_user_scoped_dashboard_metadata_by_merchant_id( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + ) + .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 update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + let mut dashboard_metadata = self.dashboard_metadata.lock().await; + + let dashboard_metadata_to_update = dashboard_metadata + .iter_mut() + .find(|metadata| { + metadata.user_id == user_id + && metadata.merchant_id == merchant_id + && metadata.org_id == org_id + && metadata.data_key == data_key + }) + .ok_or(errors::StorageError::MockDbError)?; + + match dashboard_metadata_update { + storage::DashboardMetadataUpdate::UpdateData { + data_key, + data_value, + last_modified_by, + } => { + dashboard_metadata_to_update.data_key = data_key; + dashboard_metadata_to_update.data_value = data_value; + dashboard_metadata_to_update.last_modified_by = last_modified_by; + dashboard_metadata_to_update.last_modified_at = common_utils::date_time::now(); + } + } + Ok(dashboard_metadata_to_update.clone()) + } + + 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) + } + async fn delete_user_scoped_dashboard_metadata_by_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + let mut dashboard_metadata = self.dashboard_metadata.lock().await; + + let initial_len = dashboard_metadata.len(); + + dashboard_metadata.retain(|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) + }); + + if dashboard_metadata.len() == initial_len { + return Err(errors::StorageError::ValueNotFound(format!( + "No user available for user_id = {user_id} and merchant id = {merchant_id}" + )) + .into()); + } + + Ok(true) + } +} diff --git a/crates/router/src/db/dispute.rs b/crates/router/src/db/dispute.rs index c63585205bb3..4529b121fa24 100644 --- a/crates/router/src/db/dispute.rs +++ b/crates/router/src/db/dispute.rs @@ -572,7 +572,7 @@ mod tests { assert_eq!(1, found_disputes.len()); - assert_eq!(created_dispute, found_disputes.get(0).unwrap().clone()); + assert_eq!(created_dispute, found_disputes.first().unwrap().clone()); } #[tokio::test] @@ -611,7 +611,7 @@ mod tests { assert_eq!(1, found_disputes.len()); - assert_eq!(created_dispute, found_disputes.get(0).unwrap().clone()); + assert_eq!(created_dispute, found_disputes.first().unwrap().clone()); } mod update_dispute { diff --git a/crates/router/src/db/health_check.rs b/crates/router/src/db/health_check.rs new file mode 100644 index 000000000000..6ebc9dfff5ad --- /dev/null +++ b/crates/router/src/db/health_check.rs @@ -0,0 +1,68 @@ +use async_bb8_diesel::{AsyncConnection, AsyncRunQueryDsl}; +use diesel_models::ConfigNew; +use error_stack::ResultExt; +use router_env::logger; + +use super::{MockDb, Store}; +use crate::{ + connection, + core::errors::{self, CustomResult}, + types::storage, +}; + +#[async_trait::async_trait] +pub trait HealthCheckDbInterface { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError>; +} + +#[async_trait::async_trait] +impl HealthCheckDbInterface for Store { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { + let conn = connection::pg_connection_write(self) + .await + .change_context(errors::HealthCheckDBError::DBError)?; + + conn.transaction_async(|conn| async move { + let query = diesel::select(diesel::dsl::sql::("1 + 1")); + let _x: i32 = query.get_result_async(&conn).await.map_err(|err| { + logger::error!(read_err=?err,"Error while reading element in the database"); + errors::HealthCheckDBError::DBReadError + })?; + + logger::debug!("Database read was successful"); + + let config = ConfigNew { + key: "test_key".to_string(), + config: "test_value".to_string(), + }; + + config.insert(&conn).await.map_err(|err| { + logger::error!(write_err=?err,"Error while writing to database"); + errors::HealthCheckDBError::DBWriteError + })?; + + logger::debug!("Database write was successful"); + + storage::Config::delete_by_key(&conn, "test_key") + .await + .map_err(|err| { + logger::error!(delete_err=?err,"Error while deleting element in the database"); + errors::HealthCheckDBError::DBDeleteError + })?; + + logger::debug!("Database delete was successful"); + + Ok::<_, errors::HealthCheckDBError>(()) + }) + .await?; + + Ok(()) + } +} + +#[async_trait::async_trait] +impl HealthCheckDbInterface for MockDb { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { + Ok(()) + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs new file mode 100644 index 000000000000..0a9030bae29e --- /dev/null +++ b/crates/router/src/db/kafka_store.rs @@ -0,0 +1,2201 @@ +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::{sample_data::BatchSampleDataInterface, UserInterface}, + user_role::UserRoleInterface, +}; +use crate::{ + core::errors::{self, ProcessTrackerError}, + db::{ + address::AddressInterface, + api_keys::ApiKeyInterface, + authorization::AuthorizationInterface, + business_profile::BusinessProfileInterface, + capture::CaptureInterface, + cards_info::CardsInfoInterface, + configs::ConfigInterface, + customers::CustomerInterface, + dispute::DisputeInterface, + ephemeral_key::EphemeralKeyInterface, + events::EventInterface, + file::FileMetadataInterface, + gsm::GsmInterface, + health_check::HealthCheckDbInterface, + 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 + } + + #[cfg(feature = "olap")] + async fn list_multiple_merchant_accounts( + &self, + merchant_ids: Vec, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_multiple_merchant_accounts(merchant_ids) + .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 + } + + #[cfg(feature = "olap")] + async fn list_multiple_key_stores( + &self, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_multiple_key_stores(merchant_ids, key) + .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 update_user_by_email( + &self, + user_email: &str, + user: storage::UserUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_by_email(user_email, 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 + } + + async fn find_users_and_roles_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_users_and_roles_by_merchant_id(merchant_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 find_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .find_user_role_by_user_id_merchant_id(user_id, merchant_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_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_user_role_by_user_id_merchant_id(user_id, merchant_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 update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + self.diesel_store + .update_metadata( + user_id, + merchant_id, + org_id, + data_key, + dashboard_metadata_update, + ) + .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 + } + + async fn delete_user_scoped_dashboard_metadata_by_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_user_scoped_dashboard_metadata_by_merchant_id(user_id, merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl BatchSampleDataInterface for KafkaStore { + async fn insert_payment_intents_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, data_models::errors::StorageError> + { + let payment_intents_list = self + .diesel_store + .insert_payment_intents_batch_for_sample_data(batch) + .await?; + + for payment_intent in payment_intents_list.iter() { + let _ = self + .kafka_producer + .log_payment_intent(payment_intent, None) + .await; + } + Ok(payment_intents_list) + } + + async fn insert_payment_attempts_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult< + Vec, + data_models::errors::StorageError, + > { + let payment_attempts_list = self + .diesel_store + .insert_payment_attempts_batch_for_sample_data(batch) + .await?; + + for payment_attempt in payment_attempts_list.iter() { + let _ = self + .kafka_producer + .log_payment_attempt(payment_attempt, None) + .await; + } + Ok(payment_attempts_list) + } + + async fn insert_refunds_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, data_models::errors::StorageError> { + let refunds_list = self + .diesel_store + .insert_refunds_batch_for_sample_data(batch) + .await?; + + for refund in refunds_list.iter() { + let _ = self.kafka_producer.log_refund(refund, None).await; + } + Ok(refunds_list) + } + + async fn delete_payment_intents_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, data_models::errors::StorageError> + { + let payment_intents_list = self + .diesel_store + .delete_payment_intents_for_sample_data(merchant_id) + .await?; + + for payment_intent in payment_intents_list.iter() { + let _ = self + .kafka_producer + .log_payment_intent_delete(payment_intent) + .await; + } + Ok(payment_intents_list) + } + + async fn delete_payment_attempts_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult< + Vec, + data_models::errors::StorageError, + > { + let payment_attempts_list = self + .diesel_store + .delete_payment_attempts_for_sample_data(merchant_id) + .await?; + + for payment_attempt in payment_attempts_list.iter() { + let _ = self + .kafka_producer + .log_payment_attempt_delete(payment_attempt) + .await; + } + + Ok(payment_attempts_list) + } + + async fn delete_refunds_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, data_models::errors::StorageError> { + let refunds_list = self + .diesel_store + .delete_refunds_for_sample_data(merchant_id) + .await?; + + for refund in refunds_list.iter() { + let _ = self.kafka_producer.log_refund_delete(refund).await; + } + + Ok(refunds_list) + } +} + +#[async_trait::async_trait] +impl AuthorizationInterface for KafkaStore { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult { + self.diesel_store.insert_authorization(authorization).await + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_all_authorizations_by_merchant_id_payment_id(merchant_id, payment_id) + .await + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + self.diesel_store + .update_authorization_by_merchant_id_authorization_id( + merchant_id, + authorization_id, + authorization, + ) + .await + } +} + +#[async_trait::async_trait] +impl HealthCheckDbInterface for KafkaStore { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { + self.diesel_store.health_check_db().await + } +} diff --git a/crates/router/src/db/mandate.rs b/crates/router/src/db/mandate.rs index fcd71719657b..0cf5cabf2e42 100644 --- a/crates/router/src/db/mandate.rs +++ b/crates/router/src/db/mandate.rs @@ -202,6 +202,18 @@ impl MandateInterface for MockDb { } => { mandate.connector_mandate_ids = connector_mandate_ids; } + + diesel_models::MandateUpdate::ConnectorMandateIdUpdate { + connector_mandate_id, + connector_mandate_ids, + payment_method_id, + original_payment_id, + } => { + mandate.connector_mandate_ids = connector_mandate_ids; + mandate.connector_mandate_id = connector_mandate_id; + mandate.payment_method_id = payment_method_id; + mandate.original_payment_id = original_payment_id + } } Ok(mandate.clone()) } diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 0d3ce99b948d..70d417c0366d 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "olap")] +use std::collections::HashMap; + use common_utils::ext_traits::AsyncExt; use error_stack::{IntoReport, ResultExt}; #[cfg(feature = "accounts_cache")] @@ -65,6 +68,12 @@ where &self, merchant_id: &str, ) -> CustomResult; + + #[cfg(feature = "olap")] + async fn list_multiple_merchant_accounts( + &self, + merchant_ids: Vec, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -294,6 +303,57 @@ impl MerchantAccountInterface for Store { Ok(is_deleted) } + + #[cfg(feature = "olap")] + async fn list_multiple_merchant_accounts( + &self, + merchant_ids: Vec, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + + let encrypted_merchant_accounts = + storage::MerchantAccount::list_multiple_merchant_accounts(&conn, merchant_ids) + .await + .map_err(Into::into) + .into_report()?; + + let db_master_key = self.get_master_key().to_vec().into(); + + let merchant_key_stores = self + .list_multiple_key_stores( + encrypted_merchant_accounts + .iter() + .map(|merchant_account| &merchant_account.merchant_id) + .cloned() + .collect(), + &db_master_key, + ) + .await?; + + let key_stores_by_id: HashMap<_, _> = merchant_key_stores + .iter() + .map(|key_store| (key_store.merchant_id.to_owned(), key_store)) + .collect(); + + let merchant_accounts = + futures::future::try_join_all(encrypted_merchant_accounts.into_iter().map( + |merchant_account| async { + let key_store = key_stores_by_id.get(&merchant_account.merchant_id).ok_or( + errors::StorageError::ValueNotFound(format!( + "merchant_key_store with merchant_id = {}", + merchant_account.merchant_id + )), + )?; + merchant_account + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError) + }, + )) + .await?; + + Ok(merchant_accounts) + } } #[async_trait::async_trait] @@ -392,6 +452,14 @@ impl MerchantAccountInterface for MockDb { ) -> CustomResult, errors::StorageError> { Err(errors::StorageError::MockDbError)? } + + #[cfg(feature = "olap")] + async fn list_multiple_merchant_accounts( + &self, + _merchant_ids: Vec, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } } #[cfg(feature = "accounts_cache")] diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index ecf52531f28a..3718b962b458 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) @@ -888,6 +890,7 @@ mod merchant_connector_account_cache_tests { "{}_{}", merchant_id, connector_label ),) + .await .is_none()) } } diff --git a/crates/router/src/db/merchant_key_store.rs b/crates/router/src/db/merchant_key_store.rs index 970e2b770324..630b833afdde 100644 --- a/crates/router/src/db/merchant_key_store.rs +++ b/crates/router/src/db/merchant_key_store.rs @@ -32,6 +32,13 @@ pub trait MerchantKeyStoreInterface { &self, merchant_id: &str, ) -> CustomResult; + + #[cfg(feature = "olap")] + async fn list_multiple_key_stores( + &self, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -128,6 +135,33 @@ impl MerchantKeyStoreInterface for Store { .await } } + + #[cfg(feature = "olap")] + async fn list_multiple_key_stores( + &self, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, errors::StorageError> { + let fetch_func = || async { + let conn = connection::pg_connection_read(self).await?; + + diesel_models::merchant_key_store::MerchantKeyStore::list_multiple_key_stores( + &conn, + merchant_ids, + ) + .await + .map_err(Into::into) + .into_report() + }; + + futures::future::try_join_all(fetch_func().await?.into_iter().map(|key_store| async { + key_store + .convert(key) + .await + .change_context(errors::StorageError::DecryptionError) + })) + .await + } } #[async_trait::async_trait] @@ -194,6 +228,28 @@ impl MerchantKeyStoreInterface for MockDb { merchant_key_stores.remove(index); Ok(true) } + + #[cfg(feature = "olap")] + async fn list_multiple_key_stores( + &self, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, errors::StorageError> { + let merchant_key_stores = self.merchant_key_store.lock().await; + futures::future::try_join_all( + merchant_key_stores + .iter() + .filter(|merchant_key| merchant_ids.contains(&merchant_key.merchant_id)) + .map(|merchant_key| async { + merchant_key + .to_owned() + .convert(key) + .await + .change_context(errors::StorageError::DecryptionError) + }), + ) + .await + } } #[cfg(test)] diff --git a/crates/router/src/db/payment_link.rs b/crates/router/src/db/payment_link.rs index 38b59b1d60de..a56f9dcce2ef 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] @@ -35,15 +42,27 @@ impl PaymentLinkInterface for Store { async fn insert_payment_link( &self, - payment_link_object: storage::PaymentLinkNew, + payment_link_config: storage::PaymentLinkNew, ) -> CustomResult { let conn = connection::pg_connection_write(self).await?; - payment_link_object + payment_link_config .insert(&conn) .await .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 8ac8bd106eff..accb5e8f3f94 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -267,7 +267,7 @@ mod storage { #[cfg(feature = "kv_store")] mod storage { - use common_utils::date_time; + use common_utils::{date_time, fallback_reverse_lookup_not_found}; use error_stack::{IntoReport, ResultExt}; use redis_interface::HsetnxReply; use storage_impl::redis::kv_store::{kv_wrapper, KvOperation}; @@ -275,9 +275,8 @@ mod storage { use super::RefundInterface; use crate::{ connection, - core::errors::{self, CustomResult}, + core::errors::{self, utils::RedisErrorExt, CustomResult}, db::reverse_lookup::ReverseLookupInterface, - logger, services::Store, types::storage::{self as storage_types, enums, kv}, utils::{self, db_utils}, @@ -304,10 +303,12 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{internal_reference_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("ref_inter_ref_{merchant_id}_{internal_reference_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( @@ -382,6 +383,50 @@ mod storage { }, }; + let mut reverse_lookups = vec![ + storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_ref_id_{}_{}", + created_refund.merchant_id, created_refund.refund_id + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }, + // [#492]: A discussion is required on whether this is required? + storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_inter_ref_{}_{}", + created_refund.merchant_id, created_refund.internal_reference_id + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }, + ]; + if let Some(connector_refund_id) = created_refund.to_owned().connector_refund_id + { + reverse_lookups.push(storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_connector_{}_{}_{}", + created_refund.merchant_id, + connector_refund_id, + created_refund.connector + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }) + }; + let rev_look = reverse_lookups + .into_iter() + .map(|rev| self.insert_reverse_lookup(rev, storage_scheme)); + + futures::future::try_join_all(rev_look).await?; + match kv_wrapper::( self, KvOperation::::HSetNx( @@ -392,7 +437,7 @@ mod storage { &key, ) .await - .change_context(errors::StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&key))? .try_into_hsetnx() { Ok(HsetnxReply::KeyNotSet) => Err(errors::StorageError::DuplicateValue { @@ -400,55 +445,7 @@ mod storage { key: Some(created_refund.refund_id), }) .into_report(), - Ok(HsetnxReply::KeySet) => { - let mut reverse_lookups = vec![ - storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}", - created_refund.merchant_id, created_refund.refund_id - ), - pk_id: key.clone(), - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }, - // [#492]: A discussion is required on whether this is required? - storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}", - created_refund.merchant_id, - created_refund.internal_reference_id - ), - pk_id: key.clone(), - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }, - ]; - if let Some(connector_refund_id) = - created_refund.to_owned().connector_refund_id - { - reverse_lookups.push(storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}_{}", - created_refund.merchant_id, - connector_refund_id, - created_refund.connector - ), - pk_id: key, - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }) - }; - let rev_look = reverse_lookups - .into_iter() - .map(|rev| self.insert_reverse_lookup(rev, storage_scheme)); - - futures::future::try_join_all(rev_look).await?; - - Ok(created_refund) - } + Ok(HsetnxReply::KeySet) => Ok(created_refund), Err(er) => Err(er).change_context(errors::StorageError::KVError), } } @@ -475,17 +472,14 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_transaction_id}"); - let lookup = match self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await - { - Ok(l) => l, - Err(err) => { - logger::error!(?err); - return Ok(vec![]); - } - }; + let lookup_id = + format!("pa_conn_trans_{merchant_id}_{connector_transaction_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); + let key = &lookup.pk_id; let pattern = db_utils::generate_hscan_pattern_for_refund(&lookup.sk_id); @@ -550,7 +544,7 @@ mod storage { &key, ) .await - .change_context(errors::StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&key))? .try_into_hset() .change_context(errors::StorageError::KVError)?; @@ -575,10 +569,12 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{refund_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("ref_ref_id_{merchant_id}_{refund_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( @@ -620,10 +616,13 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_refund_id}_{connector}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = + format!("ref_connector_{merchant_id}_{connector_refund_id}_{connector}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( @@ -998,7 +997,7 @@ impl RefundInterface for MockDb { let mut refund_meta_data = api_models::refunds::RefundListMetaData { connector: vec![], currency: vec![], - status: vec![], + refund_status: vec![], }; let mut unique_connectors = HashSet::new(); @@ -1017,7 +1016,7 @@ impl RefundInterface for MockDb { refund_meta_data.connector = unique_connectors.into_iter().collect(); refund_meta_data.currency = unique_currencies.into_iter().collect(); - refund_meta_data.status = unique_statuses.into_iter().collect(); + refund_meta_data.refund_status = unique_statuses.into_iter().collect(); Ok(refund_meta_data) } diff --git a/crates/router/src/db/reverse_lookup.rs b/crates/router/src/db/reverse_lookup.rs index 445e171fa277..344077f3ec0f 100644 --- a/crates/router/src/db/reverse_lookup.rs +++ b/crates/router/src/db/reverse_lookup.rs @@ -69,6 +69,7 @@ mod storage { use super::{ReverseLookupInterface, Store}; use crate::{ connection, + core::errors::utils::RedisErrorExt, errors::{self, CustomResult}, types::storage::{ enums, kv, @@ -109,7 +110,7 @@ mod storage { format!("reverse_lookup_{}", &created_rev_lookup.lookup_id), ) .await - .change_context(errors::StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&created_rev_lookup.lookup_id))? .try_into_setnx() { Ok(SetnxReply::KeySet) => Ok(created_rev_lookup), diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index 6bb1d9e50b6a..c7c005a0b52b 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -1,4 +1,4 @@ -use diesel_models::user as storage; +use diesel_models::{user as storage, user_role::UserRole}; use error_stack::{IntoReport, ResultExt}; use masking::Secret; @@ -8,6 +8,7 @@ use crate::{ core::errors::{self, CustomResult}, services::Store, }; +pub mod sample_data; #[async_trait::async_trait] pub trait UserInterface { @@ -32,10 +33,21 @@ pub trait UserInterface { user: storage::UserUpdate, ) -> CustomResult; + async fn update_user_by_email( + &self, + user_email: &str, + user: storage::UserUpdate, + ) -> CustomResult; + async fn delete_user_by_user_id( &self, user_id: &str, ) -> CustomResult; + + async fn find_users_and_roles_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -86,6 +98,18 @@ impl UserInterface for Store { .into_report() } + async fn update_user_by_email( + &self, + user_email: &str, + user: storage::UserUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::update_by_user_email(&conn, user_email, user) + .await + .map_err(Into::into) + .into_report() + } + async fn delete_user_by_user_id( &self, user_id: &str, @@ -96,6 +120,17 @@ impl UserInterface for Store { .map_err(Into::into) .into_report() } + + async fn find_users_and_roles_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::User::find_joined_users_and_roles_by_merchant_id(&conn, merchant_id) + .await + .map_err(Into::into) + .into_report() + } } #[async_trait::async_trait] @@ -128,6 +163,7 @@ impl UserInterface for MockDb { is_verified: user_data.is_verified, created_at: user_data.created_at.unwrap_or(time_now), last_modified_at: user_data.created_at.unwrap_or(time_now), + preferred_merchant_id: user_data.preferred_merchant_id, }; users.push(user.clone()); Ok(user) @@ -190,10 +226,14 @@ impl UserInterface for MockDb { name, password, is_verified, + preferred_merchant_id, } => storage::User { name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), password: password.clone().unwrap_or(user.password.clone()), is_verified: is_verified.unwrap_or(user.is_verified), + preferred_merchant_id: preferred_merchant_id + .clone() + .or(user.preferred_merchant_id.clone()), ..user.to_owned() }, }; @@ -207,6 +247,50 @@ impl UserInterface for MockDb { ) } + async fn update_user_by_email( + &self, + user_email: &str, + update_user: storage::UserUpdate, + ) -> CustomResult { + let mut users = self.users.lock().await; + let user_email_pii: common_utils::pii::Email = user_email + .to_string() + .try_into() + .map_err(|_| errors::StorageError::MockDbError)?; + users + .iter_mut() + .find(|user| user.email == user_email_pii) + .map(|user| { + *user = match &update_user { + storage::UserUpdate::VerifyUser => storage::User { + is_verified: true, + ..user.to_owned() + }, + storage::UserUpdate::AccountUpdate { + name, + password, + is_verified, + preferred_merchant_id, + } => storage::User { + name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), + password: password.clone().unwrap_or(user.password.clone()), + is_verified: is_verified.unwrap_or(user.is_verified), + preferred_merchant_id: preferred_merchant_id + .clone() + .or(user.preferred_merchant_id.clone()), + ..user.to_owned() + }, + }; + user.to_owned() + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user available for user_email = {user_email}" + )) + .into(), + ) + } + async fn delete_user_by_user_id( &self, user_id: &str, @@ -221,45 +305,11 @@ impl UserInterface for MockDb { users.remove(user_index); Ok(true) } -} -#[cfg(feature = "kafka_events")] -#[async_trait::async_trait] -impl UserInterface for super::KafkaStore { - async fn insert_user( - &self, - user_data: storage::UserNew, - ) -> CustomResult { - self.diesel_store.insert_user(user_data).await - } - async fn find_user_by_email( - &self, - user_email: &str, - ) -> CustomResult { - self.diesel_store.find_user_by_email(user_email).await - } - - async fn find_user_by_id( + async fn find_users_and_roles_by_merchant_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 + _merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? } } diff --git a/crates/router/src/db/user/sample_data.rs b/crates/router/src/db/user/sample_data.rs new file mode 100644 index 000000000000..ae98332cfc49 --- /dev/null +++ b/crates/router/src/db/user/sample_data.rs @@ -0,0 +1,199 @@ +use data_models::{ + errors::StorageError, + payments::{payment_attempt::PaymentAttempt, payment_intent::PaymentIntentNew, PaymentIntent}, +}; +use diesel_models::{ + errors::DatabaseError, + query::user::sample_data as sample_data_queries, + refund::{Refund, RefundNew}, + user::sample_data::PaymentAttemptBatchNew, +}; +use error_stack::{Report, ResultExt}; +use storage_impl::DataModelExt; + +use crate::{connection::pg_connection_write, core::errors::CustomResult, services::Store}; + +#[async_trait::async_trait] +pub trait BatchSampleDataInterface { + async fn insert_payment_intents_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError>; + + async fn insert_payment_attempts_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError>; + + async fn insert_refunds_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError>; + + async fn delete_payment_intents_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError>; + + async fn delete_payment_attempts_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError>; + + async fn delete_refunds_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError>; +} + +#[async_trait::async_trait] +impl BatchSampleDataInterface for Store { + async fn insert_payment_intents_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + let new_intents = batch.into_iter().map(|i| i.to_storage_model()).collect(); + sample_data_queries::insert_payment_intents(&conn, new_intents) + .await + .map_err(diesel_error_to_data_error) + .map(|v| { + v.into_iter() + .map(PaymentIntent::from_storage_model) + .collect() + }) + } + + async fn insert_payment_attempts_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + sample_data_queries::insert_payment_attempts(&conn, batch) + .await + .map_err(diesel_error_to_data_error) + .map(|res| { + res.into_iter() + .map(PaymentAttempt::from_storage_model) + .collect() + }) + } + async fn insert_refunds_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + sample_data_queries::insert_refunds(&conn, batch) + .await + .map_err(diesel_error_to_data_error) + } + + async fn delete_payment_intents_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + sample_data_queries::delete_payment_intents(&conn, merchant_id) + .await + .map_err(diesel_error_to_data_error) + .map(|v| { + v.into_iter() + .map(PaymentIntent::from_storage_model) + .collect() + }) + } + + async fn delete_payment_attempts_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + sample_data_queries::delete_payment_attempts(&conn, merchant_id) + .await + .map_err(diesel_error_to_data_error) + .map(|res| { + res.into_iter() + .map(PaymentAttempt::from_storage_model) + .collect() + }) + } + async fn delete_refunds_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + sample_data_queries::delete_refunds(&conn, merchant_id) + .await + .map_err(diesel_error_to_data_error) + } +} + +#[async_trait::async_trait] +impl BatchSampleDataInterface for storage_impl::MockDb { + async fn insert_payment_intents_batch_for_sample_data( + &self, + _batch: Vec, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + + async fn insert_payment_attempts_batch_for_sample_data( + &self, + _batch: Vec, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + + async fn insert_refunds_batch_for_sample_data( + &self, + _batch: Vec, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + + async fn delete_payment_intents_for_sample_data( + &self, + _merchant_id: &str, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + async fn delete_payment_attempts_for_sample_data( + &self, + _merchant_id: &str, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + async fn delete_refunds_for_sample_data( + &self, + _merchant_id: &str, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } +} + +// TODO: This error conversion is re-used from storage_impl and is not DRY when it should be +// Ideally the impl's here should be defined in that crate avoiding this re-definition +fn diesel_error_to_data_error(diesel_error: Report) -> Report { + let new_err = match diesel_error.current_context() { + DatabaseError::DatabaseConnectionError => StorageError::DatabaseConnectionError, + DatabaseError::NotFound => StorageError::ValueNotFound("Value not found".to_string()), + DatabaseError::UniqueViolation => StorageError::DuplicateValue { + entity: "entity ", + key: None, + }, + err => StorageError::DatabaseError(error_stack::report!(*err)), + }; + diesel_error.change_context(new_err) +} diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index 37e38e8afca7..f02e6d60b3bc 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -14,17 +14,29 @@ pub trait UserRoleInterface { &self, user_role: storage::UserRoleNew, ) -> CustomResult; + async fn find_user_role_by_user_id( &self, user_id: &str, ) -> CustomResult; + + async fn find_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult; + async fn update_user_role_by_user_id_merchant_id( &self, user_id: &str, merchant_id: &str, update: storage::UserRoleUpdate, ) -> CustomResult; - async fn delete_user_role(&self, user_id: &str) -> CustomResult; + async fn delete_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult; async fn list_user_roles_by_user_id( &self, @@ -57,6 +69,22 @@ impl UserRoleInterface for Store { .into_report() } + async fn find_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::UserRole::find_by_user_id_merchant_id( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + ) + .await + .map_err(Into::into) + .into_report() + } + async fn update_user_role_by_user_id_merchant_id( &self, user_id: &str, @@ -75,12 +103,20 @@ impl UserRoleInterface for Store { .into_report() } - async fn delete_user_role(&self, user_id: &str) -> CustomResult { + async fn delete_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { let conn = connection::pg_connection_write(self).await?; - storage::UserRole::delete_by_user_id(&conn, user_id.to_owned()) - .await - .map_err(Into::into) - .into_report() + storage::UserRole::delete_by_user_id_merchant_id( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + ) + .await + .map_err(Into::into) + .into_report() } async fn list_user_roles_by_user_id( @@ -123,7 +159,7 @@ impl UserRoleInterface for MockDb { status: user_role.status, created_by: user_role.created_by, created_at: user_role.created_at, - last_modified_at: user_role.last_modified_at, + last_modified: user_role.last_modified, last_modified_by: user_role.last_modified_by, org_id: user_role.org_id, }; @@ -148,6 +184,24 @@ impl UserRoleInterface for MockDb { ) } + async fn find_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + let user_roles = self.user_roles.lock().await; + user_roles + .iter() + .find(|user_role| user_role.user_id == user_id && user_role.merchant_id == merchant_id) + .cloned() + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user role available for user_id = {user_id} and merchant_id = {merchant_id}" + )) + .into(), + ) + } + async fn update_user_role_by_user_id_merchant_id( &self, user_id: &str, @@ -187,11 +241,17 @@ impl UserRoleInterface for MockDb { ) } - async fn delete_user_role(&self, user_id: &str) -> CustomResult { + async fn delete_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { let mut user_roles = self.user_roles.lock().await; let user_role_index = user_roles .iter() - .position(|user_role| user_role.user_id == user_id) + .position(|user_role| { + user_role.user_id == user_id && user_role.merchant_id == merchant_id + }) .ok_or(errors::StorageError::ValueNotFound(format!( "No user available for user_id = {user_id}" )))?; @@ -243,8 +303,14 @@ impl UserRoleInterface for super::KafkaStore { ) -> CustomResult { self.diesel_store.find_user_role_by_user_id(user_id).await } - async fn delete_user_role(&self, user_id: &str) -> CustomResult { - self.diesel_store.delete_user_role(user_id).await + async fn delete_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_user_role_by_user_id_merchant_id(user_id, merchant_id) + .await } async fn list_user_roles_by_user_id( &self, diff --git a/crates/router/src/events.rs b/crates/router/src/events.rs index 39a8543a68c4..0091de588f13 100644 --- a/crates/router/src/events.rs +++ b/crates/router/src/events.rs @@ -1,15 +1,23 @@ -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 connector_api_logs; pub mod event_logger; +pub mod kafka_handler; +pub mod outgoing_webhook_logs; -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, @@ -23,4 +31,58 @@ pub enum EventType { PaymentAttempt, Refund, ApiLogs, + ConnectorApiLogs, + OutgoingWebhookLogs, +} + +#[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..3d74a0288404 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: String, } 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: &http::Method, ) -> 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: http_method.to_string(), } } } @@ -105,9 +114,7 @@ impl_misc_api_event_type!( CreateFileRequest, FileId, AttachEvidenceRequest, - DisputeId, PaymentLinkFormData, - PaymentsRedirectResponseData, ConfigUpdate ); @@ -122,3 +129,23 @@ impl_misc_api_event_type!( DummyConnectorRefundResponse, DummyConnectorRefundRequest ); + +impl ApiEventMetric for PaymentsRedirectResponseData { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentRedirectionResponse { + connector: self.connector.clone(), + payment_id: match &self.resource_id { + api_models::payments::PaymentIdType::PaymentIntentId(id) => Some(id.clone()), + _ => None, + }, + }) + } +} + +impl ApiEventMetric for DisputeId { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Dispute { + dispute_id: self.dispute_id.clone(), + }) + } +} diff --git a/crates/router/src/events/connector_api_logs.rs b/crates/router/src/events/connector_api_logs.rs new file mode 100644 index 000000000000..4d3aadc3c456 --- /dev/null +++ b/crates/router/src/events/connector_api_logs.rs @@ -0,0 +1,78 @@ +use common_utils::request::Method; +use router_env::tracing_actix_web::RequestId; +use serde::Serialize; +use time::OffsetDateTime; + +use super::{EventType, RawEvent}; + +#[derive(Debug, Serialize)] +pub struct ConnectorEvent { + connector_name: String, + flow: String, + request: String, + response: Option, + url: String, + method: String, + payment_id: String, + merchant_id: String, + created_at: i128, + request_id: String, + latency: u128, + refund_id: Option, + dispute_id: Option, + status_code: u16, +} + +impl ConnectorEvent { + #[allow(clippy::too_many_arguments)] + pub fn new( + connector_name: String, + flow: &str, + request: serde_json::Value, + response: Option, + url: String, + method: Method, + payment_id: String, + merchant_id: String, + request_id: Option<&RequestId>, + latency: u128, + refund_id: Option, + dispute_id: Option, + status_code: u16, + ) -> Self { + Self { + connector_name, + flow: flow + .rsplit_once("::") + .map(|(_, s)| s) + .unwrap_or(flow) + .to_string(), + request: request.to_string(), + response, + url, + method: method.to_string(), + payment_id, + merchant_id, + created_at: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, + request_id: request_id + .map(|i| i.as_hyphenated().to_string()) + .unwrap_or("NO_REQUEST_ID".to_string()), + latency, + refund_id, + dispute_id, + status_code, + } + } +} + +impl TryFrom for RawEvent { + type Error = serde_json::Error; + + fn try_from(value: ConnectorEvent) -> Result { + Ok(Self { + event_type: EventType::ConnectorApiLogs, + key: value.request_id.clone(), + payload: serde_json::to_value(value)?, + }) + } +} diff --git a/crates/router/src/events/event_logger.rs b/crates/router/src/events/event_logger.rs index 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/events/outgoing_webhook_logs.rs b/crates/router/src/events/outgoing_webhook_logs.rs new file mode 100644 index 000000000000..ebf36caf706e --- /dev/null +++ b/crates/router/src/events/outgoing_webhook_logs.rs @@ -0,0 +1,110 @@ +use api_models::{enums::EventType as OutgoingWebhookEventType, webhooks::OutgoingWebhookContent}; +use serde::Serialize; +use serde_json::Value; +use time::OffsetDateTime; + +use super::{EventType, RawEvent}; + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct OutgoingWebhookEvent { + merchant_id: String, + event_id: String, + event_type: OutgoingWebhookEventType, + #[serde(flatten)] + content: Option, + is_error: bool, + error: Option, + created_at_timestamp: i128, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(tag = "outgoing_webhook_event_type", rename_all = "snake_case")] +pub enum OutgoingWebhookEventContent { + Payment { + payment_id: Option, + content: Value, + }, + Refund { + payment_id: String, + refund_id: String, + content: Value, + }, + Dispute { + payment_id: String, + attempt_id: String, + dispute_id: String, + content: Value, + }, + Mandate { + payment_method_id: String, + mandate_id: String, + content: Value, + }, +} +pub trait OutgoingWebhookEventMetric { + fn get_outgoing_webhook_event_type(&self) -> Option; +} +impl OutgoingWebhookEventMetric for OutgoingWebhookContent { + fn get_outgoing_webhook_event_type(&self) -> Option { + match self { + Self::PaymentDetails(payment_payload) => Some(OutgoingWebhookEventContent::Payment { + payment_id: payment_payload.payment_id.clone(), + content: masking::masked_serialize(&payment_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + Self::RefundDetails(refund_payload) => Some(OutgoingWebhookEventContent::Refund { + payment_id: refund_payload.payment_id.clone(), + refund_id: refund_payload.refund_id.clone(), + content: masking::masked_serialize(&refund_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + Self::DisputeDetails(dispute_payload) => Some(OutgoingWebhookEventContent::Dispute { + payment_id: dispute_payload.payment_id.clone(), + attempt_id: dispute_payload.attempt_id.clone(), + dispute_id: dispute_payload.dispute_id.clone(), + content: masking::masked_serialize(&dispute_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + Self::MandateDetails(mandate_payload) => Some(OutgoingWebhookEventContent::Mandate { + payment_method_id: mandate_payload.payment_method_id.clone(), + mandate_id: mandate_payload.mandate_id.clone(), + content: masking::masked_serialize(&mandate_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + } + } +} + +impl OutgoingWebhookEvent { + pub fn new( + merchant_id: String, + event_id: String, + event_type: OutgoingWebhookEventType, + content: Option, + is_error: bool, + error: Option, + ) -> Self { + Self { + merchant_id, + event_id, + event_type, + content, + is_error, + error, + created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, + } + } +} + +impl TryFrom for RawEvent { + type Error = serde_json::Error; + + fn try_from(value: OutgoingWebhookEvent) -> Result { + Ok(Self { + event_type: EventType::OutgoingWebhookLogs, + key: value.merchant_id.clone(), + payload: serde_json::to_value(value)?, + }) + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 58d77d9e02f4..bb56a173da37 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; @@ -14,12 +12,14 @@ pub mod cors; pub mod db; pub mod env; 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; pub mod services; pub mod types; pub mod utils; @@ -35,10 +35,8 @@ use storage_impl::errors::ApplicationResult; use tokio::sync::{mpsc, oneshot}; pub use self::env::logger; -use crate::{ - configs::settings, - core::errors::{self}, -}; +pub(crate) use self::macros::*; +use crate::{configs::settings, core::errors}; #[cfg(feature = "mimalloc")] #[global_allocator] @@ -95,15 +93,6 @@ pub fn mk_app( > { let mut server_app = get_application_builder(request_body_limit); - #[cfg(feature = "openapi")] - { - use utoipa::OpenApi; - server_app = server_app.service( - utoipa_swagger_ui::SwaggerUi::new("/docs/{_:.*}") - .url("/docs/openapi.json", openapi::ApiDoc::openapi()), - ); - } - #[cfg(feature = "dummy_connector")] { use routes::DummyConnector; @@ -122,6 +111,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())) @@ -130,10 +120,9 @@ pub fn mk_app( #[cfg(feature = "oltp")] { server_app = server_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())); + .service(routes::PaymentMethods::server(state.clone())) } #[cfg(feature = "olap")] @@ -145,9 +134,12 @@ pub fn mk_app( .service(routes::Disputes::server(state.clone())) .service(routes::Analytics::server(state.clone())) .service(routes::Routing::server(state.clone())) + .service(routes::Blocklist::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())) + .service(routes::ConnectorOnboarding::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] @@ -164,6 +156,12 @@ pub fn mk_app( { server_app = server_app.service(routes::StripeApis::server(state.clone())); } + + #[cfg(feature = "recon")] + { + server_app = server_app.service(routes::Recon::server(state.clone())); + } + server_app = server_app.service(routes::Cards::server(state.clone())); server_app = server_app.service(routes::Cache::server(state.clone())); server_app = server_app.service(routes::Health::server(state)); @@ -260,5 +258,8 @@ pub fn get_application_builder( .wrap(middleware::default_response_headers()) .wrap(middleware::RequestId) .wrap(cors::cors()) + .wrap(middleware::LogSpanInitializer) + // this middleware works only for Http1.1 requests + .wrap(middleware::Http400RequestDetailsLogger) .wrap(router_env::tracing_actix_web::TracingLogger::default()) } diff --git a/crates/router/src/macros.rs b/crates/router/src/macros.rs index 33ed43fcc7ab..efe71e49bb04 100644 --- a/crates/router/src/macros.rs +++ b/crates/router/src/macros.rs @@ -1,68 +1 @@ -#[macro_export] -macro_rules! newtype_impl { - ($is_pub:vis, $name:ident, $ty_path:path) => { - impl std::ops::Deref for $name { - type Target = $ty_path; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl std::ops::DerefMut for $name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - impl From<$ty_path> for $name { - fn from(ty: $ty_path) -> Self { - Self(ty) - } - } - - impl $name { - pub fn into_inner(self) -> $ty_path { - self.0 - } - } - }; -} - -#[macro_export] -macro_rules! newtype { - ($is_pub:vis $name:ident = $ty_path:path) => { - $is_pub struct $name(pub $ty_path); - - $crate::newtype_impl!($is_pub, $name, $ty_path); - }; - - ($is_pub:vis $name:ident = $ty_path:path, derives = ($($trt:path),*)) => { - #[derive($($trt),*)] - $is_pub struct $name(pub $ty_path); - - $crate::newtype_impl!($is_pub, $name, $ty_path); - }; -} - -#[macro_export] -macro_rules! async_spawn { - ($t:block) => { - tokio::spawn(async move { $t }); - }; -} - -#[macro_export] -macro_rules! collect_missing_value_keys { - [$(($key:literal, $option:expr)),+] => { - { - let mut keys: Vec<&'static str> = Vec::new(); - $( - if $option.is_none() { - keys.push($key); - } - )* - keys - } - }; -} +pub use common_utils::{collect_missing_value_keys, newtype}; diff --git a/crates/router/src/middleware.rs b/crates/router/src/middleware.rs index 1576432e26ed..702c3c70e6c6 100644 --- a/crates/router/src/middleware.rs +++ b/crates/router/src/middleware.rs @@ -1,3 +1,8 @@ +use futures::StreamExt; +use router_env::{ + logger, + tracing::{field::Empty, Instrument}, +}; /// Middleware to include request ID in response header. pub struct RequestId; @@ -43,20 +48,28 @@ where actix_web::dev::forward_ready!(service); fn call(&self, req: actix_web::dev::ServiceRequest) -> Self::Future { + let old_x_request_id = req.headers().get("x-request-id").cloned(); let mut req = req; let request_id_fut = req.extract::(); let response_fut = self.service.call(req); - Box::pin(async move { - let request_id = request_id_fut.await?; - let mut response = response_fut.await?; - response.headers_mut().append( - http::header::HeaderName::from_static("x-request-id"), - http::HeaderValue::from_str(&request_id.as_hyphenated().to_string())?, - ); + Box::pin( + async move { + let request_id = request_id_fut.await?; + let request_id = request_id.as_hyphenated().to_string(); + if let Some(upstream_request_id) = old_x_request_id { + router_env::logger::info!(?upstream_request_id); + } + let mut response = response_fut.await?; + response.headers_mut().append( + http::header::HeaderName::from_static("x-request-id"), + http::HeaderValue::from_str(&request_id)?, + ); - Ok(response) - }) + Ok(response) + } + .in_current_span(), + ) } } @@ -65,8 +78,190 @@ where pub fn default_response_headers() -> actix_web::middleware::DefaultHeaders { use actix_web::http::header; - actix_web::middleware::DefaultHeaders::new() + let default_headers_middleware = actix_web::middleware::DefaultHeaders::new(); + + #[cfg(feature = "vergen")] + let default_headers_middleware = + default_headers_middleware.add(("x-hyperswitch-version", router_env::git_tag!())); + + default_headers_middleware // Max age of 1 year in seconds, equal to `60 * 60 * 24 * 365` seconds. .add((header::STRICT_TRANSPORT_SECURITY, "max-age=31536000")) .add((header::VIA, "HyperSwitch")) } + +/// Middleware to build a TOP level domain span for each request. +pub struct LogSpanInitializer; + +impl actix_web::dev::Transform for LogSpanInitializer +where + S: actix_web::dev::Service< + actix_web::dev::ServiceRequest, + Response = actix_web::dev::ServiceResponse, + Error = actix_web::Error, + >, + S::Future: 'static, + B: 'static, +{ + type Response = actix_web::dev::ServiceResponse; + type Error = actix_web::Error; + type Transform = LogSpanInitializerMiddleware; + type InitError = (); + type Future = std::future::Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + std::future::ready(Ok(LogSpanInitializerMiddleware { service })) + } +} + +pub struct LogSpanInitializerMiddleware { + service: S, +} + +impl actix_web::dev::Service + for LogSpanInitializerMiddleware +where + S: actix_web::dev::Service< + actix_web::dev::ServiceRequest, + Response = actix_web::dev::ServiceResponse, + Error = actix_web::Error, + >, + S::Future: 'static, + B: 'static, +{ + type Response = actix_web::dev::ServiceResponse; + type Error = actix_web::Error; + type Future = futures::future::LocalBoxFuture<'static, Result>; + + actix_web::dev::forward_ready!(service); + + // TODO: have a common source of truth for the list of top level fields + // /crates/router_env/src/logger/storage.rs also has a list of fields called PERSISTENT_KEYS + fn call(&self, req: actix_web::dev::ServiceRequest) -> Self::Future { + let response_fut = self.service.call(req); + + Box::pin( + response_fut.instrument( + router_env::tracing::info_span!( + "golden_log_line", + payment_id = Empty, + merchant_id = Empty, + connector_name = Empty, + flow = "UNKNOWN" + ) + .or_current(), + ), + ) + } +} + +fn get_request_details_from_value(json_value: &serde_json::Value, parent_key: &str) -> String { + match json_value { + serde_json::Value::Null => format!("{}: null", parent_key), + serde_json::Value::Bool(b) => format!("{}: {}", parent_key, b), + serde_json::Value::Number(num) => format!("{}: {}", parent_key, num.to_string().len()), + serde_json::Value::String(s) => format!("{}: {}", parent_key, s.len()), + serde_json::Value::Array(arr) => { + let mut result = String::new(); + for (index, value) in arr.iter().enumerate() { + let child_key = format!("{}[{}]", parent_key, index); + result.push_str(&get_request_details_from_value(value, &child_key)); + if index < arr.len() - 1 { + result.push_str(", "); + } + } + result + } + serde_json::Value::Object(obj) => { + let mut result = String::new(); + for (index, (key, value)) in obj.iter().enumerate() { + let child_key = format!("{}[{}]", parent_key, key); + result.push_str(&get_request_details_from_value(value, &child_key)); + if index < obj.len() - 1 { + result.push_str(", "); + } + } + result + } + } +} + +/// Middleware for Logging request_details of HTTP 400 Bad Requests +pub struct Http400RequestDetailsLogger; + +impl actix_web::dev::Transform + for Http400RequestDetailsLogger +where + S: actix_web::dev::Service< + actix_web::dev::ServiceRequest, + Response = actix_web::dev::ServiceResponse, + Error = actix_web::Error, + >, + S::Future: 'static, + B: 'static, +{ + type Response = actix_web::dev::ServiceResponse; + type Error = actix_web::Error; + type Transform = Http400RequestDetailsLoggerMiddleware; + type InitError = (); + type Future = std::future::Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + std::future::ready(Ok(Http400RequestDetailsLoggerMiddleware { + service: std::rc::Rc::new(service), + })) + } +} + +pub struct Http400RequestDetailsLoggerMiddleware { + service: std::rc::Rc, +} + +impl actix_web::dev::Service + for Http400RequestDetailsLoggerMiddleware +where + S: actix_web::dev::Service< + actix_web::dev::ServiceRequest, + Response = actix_web::dev::ServiceResponse, + Error = actix_web::Error, + > + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = actix_web::dev::ServiceResponse; + type Error = actix_web::Error; + type Future = futures::future::LocalBoxFuture<'static, Result>; + + actix_web::dev::forward_ready!(service); + + fn call(&self, mut req: actix_web::dev::ServiceRequest) -> Self::Future { + let svc = self.service.clone(); + let request_id_fut = req.extract::(); + Box::pin(async move { + let (http_req, payload) = req.into_parts(); + let result_payload: Vec> = + payload.collect().await; + let payload = result_payload + .into_iter() + .collect::, actix_web::error::PayloadError>>()?; + let bytes = payload.clone().concat().to_vec(); + // we are creating h1 payload manually from bytes, currently there's no way to create http2 payload with actix + let (_, mut new_payload) = actix_http::h1::Payload::create(true); + new_payload.unread_data(bytes.to_vec().clone().into()); + let new_req = actix_web::dev::ServiceRequest::from_parts(http_req, new_payload.into()); + let response_fut = svc.call(new_req); + let response = response_fut.await?; + // Log the request_details when we receive 400 status from the application + if response.status() == 400 { + let value: serde_json::Value = serde_json::from_slice(&bytes)?; + let request_id = request_id_fut.await?.as_hyphenated().to_string(); + logger::info!( + "request_id: {}, request_details: {}", + request_id, + get_request_details_from_value(&value, "") + ); + } + Ok(response) + }) + } +} diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 5166e326fb91..d9916f98e745 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -1,15 +1,23 @@ pub mod admin; pub mod api_keys; pub mod app; +#[cfg(feature = "olap")] +pub mod blocklist; pub mod cache; pub mod cards_info; pub mod configs; +#[cfg(feature = "olap")] +pub mod connector_onboarding; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub mod currency; pub mod customers; pub mod disputes; #[cfg(feature = "dummy_connector")] pub mod dummy_connector; pub mod ephemeral_key; pub mod files; +#[cfg(feature = "frm")] +pub mod fraud_check; pub mod gsm; pub mod health; pub mod lock_utils; @@ -20,28 +28,41 @@ pub mod payment_methods; pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; +#[cfg(feature = "recon")] +pub mod recon; pub mod refunds; #[cfg(feature = "olap")] 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(any(feature = "olap", feature = "oltp"))] +pub mod pm_auth; +#[cfg(feature = "olap")] +pub use app::{Blocklist, Routing}; + #[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")] -pub use self::app::Routing; +#[cfg(all(feature = "olap", feature = "recon"))] +pub use self::app::Recon; #[cfg(all(feature = "olap", feature = "kms"))] pub use self::app::Verify; pub use self::app::{ - ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, - Files, Gsm, Health, LockerMigrate, Mandates, MerchantAccount, MerchantConnectorAccount, - PaymentLink, PaymentMethods, Payments, Refunds, User, Webhooks, + ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, ConnectorOnboarding, Customers, + Disputes, EphemeralKey, 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 eef8cacc5f92..f5a6a49b9c95 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, }; @@ -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, @@ -141,6 +144,7 @@ pub async fn update_merchant_account( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), @@ -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( @@ -220,6 +224,7 @@ pub async fn payment_connector_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantConnectorAccountWrite, }, req.headers(), ), @@ -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, @@ -349,7 +360,7 @@ pub async fn payment_connector_update( let flow = Flow::MerchantConnectorsUpdate; let (merchant_id, merchant_connector_id) = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -359,11 +370,12 @@ pub async fn payment_connector_update( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantConnectorAccountWrite, }, req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Merchant Connector - Delete @@ -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, @@ -460,6 +475,7 @@ pub async fn business_profile_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), @@ -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, @@ -501,7 +520,7 @@ pub async fn business_profile_update( let flow = Flow::BusinessProfileUpdate; let (merchant_id, profile_id) = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -511,11 +530,12 @@ pub async fn business_profile_update( &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::BusinessProfileDelete))] @@ -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 7299aa696390..fb1851af00d9 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -1,10 +1,12 @@ use actix_web::{web, HttpRequest, Responder}; +#[cfg(feature = "hashicorp-vault")] +use error_stack::ResultExt; 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, }; @@ -44,10 +46,20 @@ pub async fn api_key_create( |state, _, payload| async { #[cfg(feature = "kms")] let kms_client = external_services::kms::get_kms_client(&state.clone().conf.kms).await; + + #[cfg(feature = "hashicorp-vault")] + let hc_client = external_services::hashicorp_vault::get_hashicorp_client( + &state.clone().conf.hc_vault, + ) + .await + .change_context(crate::core::errors::ApiErrorResponse::InternalServerError)?; + api_keys::create_api_key( state, #[cfg(feature = "kms")] kms_client, + #[cfg(feature = "hashicorp-vault")] + hc_client, payload, merchant_id.clone(), ) @@ -57,6 +69,7 @@ pub async fn api_key_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyWrite, }, req.headers(), ), @@ -101,6 +114,7 @@ pub async fn api_key_retrieve( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyRead, }, req.headers(), ), @@ -138,7 +152,7 @@ pub async fn api_key_update( let (merchant_id, key_id) = path.into_inner(); let mut payload = json_payload.into_inner(); payload.key_id = key_id; - payload.merchant_id = merchant_id; + payload.merchant_id = merchant_id.clone(); api::server_wrap( flow, @@ -146,7 +160,14 @@ pub async fn api_key_update( &req, payload, |state, _, payload| api_keys::update_api_key(state, payload), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::ApiKeyWrite, + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -189,6 +210,7 @@ pub async fn api_key_revoke( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyWrite, }, req.headers(), ), @@ -237,7 +259,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 070f1eb29bf8..9e8bee73c286 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1,34 +1,56 @@ use std::sync::Arc; use actix_web::{web, Scope}; +#[cfg(all(feature = "olap", any(feature = "hashicorp-vault", feature = "kms")))] +use analytics::AnalyticsConfig; #[cfg(feature = "email")] -use external_services::email::{AwsSes, EmailClient}; +use external_services::email::{ses::AwsSes, EmailService}; +use external_services::file_storage::FileStorageInterface; +#[cfg(all(feature = "olap", feature = "hashicorp-vault"))] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; +#[cfg(all(feature = "olap", feature = "kms"))] +use masking::PeekInterface; use router_env::tracing_actix_web::RequestId; use scheduler::SchedulerInterface; use storage_impl::MockDb; use tokio::sync::oneshot; +#[cfg(feature = "olap")] +use super::blocklist; +#[cfg(any(feature = "olap", feature = "oltp"))] +use super::currency; #[cfg(feature = "dummy_connector")] use super::dummy_connector::*; #[cfg(feature = "payouts")] use super::payouts::*; +#[cfg(feature = "oltp")] +use super::pm_auth; #[cfg(feature = "olap")] use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, user::*}; -use super::{cache::*, health::*, payment_link::*}; +use super::{ + admin::*, api_keys::*, connector_onboarding::*, 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(all(feature = "frm", feature = "oltp"))] +use crate::routes::fraud_check as frm_routes; +#[cfg(all(feature = "recon", feature = "olap"))] +use crate::routes::recon as recon_routes; +#[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,14 +60,16 @@ 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, #[cfg(feature = "olap")] pub pool: crate::analytics::AnalyticsProvider, + pub request_id: Option, + pub file_storage_client: Box, } impl scheduler::SchedulerAppState for AppState { @@ -57,9 +81,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,15 +98,16 @@ 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); - self.store.add_request_id(request_id.to_string()) + self.store.add_request_id(request_id.to_string()); + self.request_id.replace(request_id); } fn add_merchant_id(&mut self, merchant_id: Option) { @@ -102,12 +127,22 @@ 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 /// /// Panics if Store can't be created or JWE decryption fails pub async fn with_storage( - conf: settings::Settings, + #[cfg_attr(not(all(feature = "olap", feature = "kms")), allow(unused_mut))] + mut conf: settings::Settings, storage_impl: StorageImpl, shut_down_signal: oneshot::Sender<()>, api_client: Box, @@ -115,14 +150,39 @@ impl AppState { Box::pin(async move { #[cfg(feature = "kms")] let kms_client = kms::get_kms_client(&conf.kms).await; + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + let hc_client = + external_services::hashicorp_vault::get_hashicorp_client(&conf.hc_vault) + .await + .expect("Failed while creating hashicorp_client"); let testable = storage_impl == StorageImpl::PostgresqlTest; + #[allow(clippy::expect_used)] + 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 => Box::new( - #[allow(clippy::expect_used)] - get_store(&conf, shut_down_signal, testable) - .await - .expect("Failed to create store"), - ), + 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) @@ -131,13 +191,70 @@ impl AppState { ), }; + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + match conf.analytics { + AnalyticsConfig::Clickhouse { .. } => {} + AnalyticsConfig::Sqlx { ref mut sqlx } + | AnalyticsConfig::CombinedCkh { ref mut sqlx, .. } + | AnalyticsConfig::CombinedSqlx { ref mut sqlx, .. } => { + sqlx.password = sqlx + .password + .clone() + .fetch_inner::(hc_client) + .await + .expect("Failed while fetching from hashicorp vault"); + } + }; + + #[cfg(all(feature = "kms", feature = "olap"))] + #[allow(clippy::expect_used)] + match conf.analytics { + AnalyticsConfig::Clickhouse { .. } => {} + AnalyticsConfig::Sqlx { ref mut sqlx } + | AnalyticsConfig::CombinedCkh { ref mut sqlx, .. } + | AnalyticsConfig::CombinedSqlx { ref mut sqlx, .. } => { + sqlx.password = kms_client + .decrypt(&sqlx.password.peek()) + .await + .expect("Failed to decrypt password") + .into(); + } + }; + + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + { + conf.connector_onboarding = conf + .connector_onboarding + .fetch_inner::(hc_client) + .await + .expect("Failed to decrypt connector onboarding credentials"); + } + + #[cfg(all(feature = "kms", feature = "olap"))] + #[allow(clippy::expect_used)] + { + conf.connector_onboarding = conf + .connector_onboarding + .decrypt_inner(kms_client) + .await + .expect("Failed to decrypt connector onboarding credentials"); + } + #[cfg(feature = "olap")] - let pool = crate::analytics::AnalyticsProvider::from_conf( - &conf.analytics, - #[cfg(feature = "kms")] - kms_client, - ) - .await; + let pool = crate::analytics::AnalyticsProvider::from_conf(&conf.analytics).await; + + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + { + conf.jwekey = conf + .jwekey + .clone() + .fetch_inner::(hc_client) + .await + .expect("Failed to decrypt connector onboarding credentials"); + } #[cfg(feature = "kms")] #[allow(clippy::expect_used)] @@ -149,7 +266,10 @@ impl AppState { .expect("Failed while performing KMS decryption"); #[cfg(feature = "email")] - let email_client = Arc::new(AwsSes::new(&conf.email).await); + let email_client = Arc::new(create_email_client(&conf).await); + + let file_storage_client = conf.file_storage.get_file_storage_client().await; + Self { flow_name: String::from("default"), store, @@ -159,9 +279,11 @@ impl AppState { #[cfg(feature = "kms")] kms_secrets: Arc::new(kms_secrets), api_client, - event_handler: Box::::default(), + event_handler, #[cfg(feature = "olap")] pool, + request_id: None, + file_storage_client, } }) .await @@ -186,9 +308,10 @@ pub struct Health; impl Health { pub fn server(state: AppState) -> Scope { - web::scope("") + web::scope("health") .app_data(web::Data::new(state)) - .service(web::resource("/health").route(web::get().to(health))) + .service(web::resource("").route(web::get().to(health))) + .service(web::resource("/ready").route(web::get().to(deep_health_check))) } } @@ -274,6 +397,14 @@ impl Payments { .service( web::resource("/{payment_id}/capture").route(web::post().to(payments_capture)), ) + .service( + web::resource("/{payment_id}/approve") + .route(web::post().to(payments_approve)), + ) + .service( + web::resource("/{payment_id}/reject") + .route(web::post().to(payments_reject)), + ) .service( web::resource("/redirect/{payment_id}/{merchant_id}/{attempt_id}") .route(web::get().to(payments_start)), @@ -293,12 +424,31 @@ impl Payments { web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}") .route(web::get().to(payments_complete_authorize)) .route(web::post().to(payments_complete_authorize)), + ) + .service( + web::resource("/{payment_id}/incremental_authorization").route(web::post().to(payments_incremental_authorization)), ); } route } } +#[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; @@ -313,7 +463,7 @@ impl Routing { ) .service( web::resource("") - .route(web::get().to(cloud_routing::routing_retrieve_dictionary)) + .route(web::get().to(cloud_routing::list_routing_configs)) .route(web::post().to(cloud_routing::routing_create_config)), ) .service( @@ -325,6 +475,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)), @@ -452,6 +616,45 @@ impl PaymentMethods { .route(web::post().to(payment_method_update_api)) .route(web::delete().to(payment_method_delete_api)), ) + .service(web::resource("/auth/link").route(web::post().to(pm_auth::link_token_create))) + .service(web::resource("/auth/exchange").route(web::post().to(pm_auth::exchange_token))) + } +} + +#[cfg(all(feature = "olap", feature = "recon"))] +pub struct Recon; + +#[cfg(all(feature = "olap", feature = "recon"))] +impl Recon { + pub fn server(state: AppState) -> Scope { + web::scope("/recon") + .app_data(web::Data::new(state)) + .service( + web::resource("/update_merchant") + .route(web::post().to(recon_routes::update_merchant)), + ) + .service(web::resource("/token").route(web::get().to(recon_routes::get_recon_token))) + .service( + web::resource("/request").route(web::post().to(recon_routes::request_for_recon)), + ) + .service(web::resource("/verify_token").route(web::get().to(verify_recon_token))) + } +} + +#[cfg(feature = "olap")] +pub struct Blocklist; + +#[cfg(feature = "olap")] +impl Blocklist { + pub fn server(state: AppState) -> Scope { + web::scope("/blocklist") + .app_data(web::Data::new(state)) + .service( + web::resource("") + .route(web::get().to(blocklist::list_blocked_payment_methods)) + .route(web::post().to(blocklist::add_entry_to_blocklist)) + .route(web::delete().to(blocklist::remove_entry_from_blocklist)), + ) } } @@ -490,6 +693,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)) @@ -553,7 +760,8 @@ impl Webhooks { pub fn server(config: AppState) -> Scope { use api_models::webhooks as webhook_type; - web::scope("/webhooks") + #[allow(unused_mut)] + let mut route = web::scope("/webhooks") .app_data(web::Data::new(config)) .service( web::resource("/{merchant_id}/{connector_id_or_name}") @@ -564,7 +772,17 @@ impl Webhooks { .route( web::put().to(receive_incoming_webhook::), ), - ) + ); + + #[cfg(feature = "frm")] + { + route = route.service( + web::resource("/frm_fulfillment") + .route(web::post().to(frm_routes::frm_fulfillment)), + ); + } + + route } } @@ -661,11 +879,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)), ) @@ -673,6 +892,10 @@ impl PaymentLink { web::resource("{merchant_id}/{payment_id}") .route(web::get().to(initiate_payment_link)), ) + .service( + web::resource("status/{merchant_id}/{payment_id}") + .route(web::get().to(payment_link_status)), + ) } } @@ -735,12 +958,86 @@ pub struct User; #[cfg(feature = "olap")] impl User { pub fn server(state: AppState) -> Scope { - web::scope("/user") - .app_data(web::Data::new(state)) - .service(web::resource("/signin").route(web::post().to(user_connect_account))) - .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))) + let mut route = web::scope("/user").app_data(web::Data::new(state)); + + route = route + .service( + web::resource("/signin").route(web::post().to(user_signin_without_invite_checks)), + ) + .service(web::resource("/v2/signin").route(web::post().to(user_signin))) + .service(web::resource("/signout").route(web::post().to(signout))) + .service(web::resource("/change_password").route(web::post().to(change_password))) + .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)), + ) + .service(web::resource("/switch/list").route(web::get().to(list_merchant_ids_for_user))) + .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) + .service(web::resource("/update").route(web::post().to(update_user_account_details))) + .service( + web::resource("/user/invite_multiple").route(web::post().to(invite_multiple_user)), + ) + .service( + web::resource("/data") + .route(web::get().to(get_multiple_dashboard_metadata)) + .route(web::post().to(set_dashboard_metadata)), + ) + .service(web::resource("/user/delete").route(web::delete().to(delete_user_role))); + + // User management + route = route.service( + web::scope("/user") + .service(web::resource("/list").route(web::get().to(get_user_details))) + .service(web::resource("/invite").route(web::post().to(invite_user))) + .service(web::resource("/invite/accept").route(web::post().to(accept_invitation))) + .service(web::resource("/update_role").route(web::post().to(update_user_role))), + ); + + // Role information + route = route.service( + web::scope("/role") + .service(web::resource("").route(web::get().to(get_role_from_token))) + .service(web::resource("/list").route(web::get().to(list_all_roles))) + .service(web::resource("/{role_id}").route(web::get().to(get_role))), + ); + + #[cfg(feature = "dummy_connector")] + { + route = route.service( + web::resource("/sample_data") + .route(web::post().to(generate_sample_data)) + .route(web::delete().to(delete_sample_data)), + ) + } + #[cfg(feature = "email")] + { + route = route + .service( + web::resource("/connect_account").route(web::post().to(user_connect_account)), + ) + .service(web::resource("/forgot_password").route(web::post().to(forgot_password))) + .service(web::resource("/reset_password").route(web::post().to(reset_password))) + .service( + web::resource("/signup_with_merchant_id") + .route(web::post().to(user_signup_with_merchant_id)), + ) + .service( + web::resource("/verify_email") + .route(web::post().to(verify_email_without_invite_checks)), + ) + .service(web::resource("/v2/verify_email").route(web::post().to(verify_email))) + .service( + web::resource("/verify_email_request") + .route(web::post().to(verify_email_request)), + ); + } + #[cfg(not(feature = "email"))] + { + route = route.service(web::resource("/signup").route(web::post().to(user_signup))) + } + route } } @@ -756,3 +1053,16 @@ impl LockerMigrate { ) } } + +pub struct ConnectorOnboarding; + +#[cfg(feature = "olap")] +impl ConnectorOnboarding { + pub fn server(state: AppState) -> Scope { + web::scope("/connector_onboarding") + .app_data(web::Data::new(state)) + .service(web::resource("/action_url").route(web::post().to(get_action_url))) + .service(web::resource("/sync").route(web::post().to(sync_onboarding_status))) + .service(web::resource("/reset_tracking_id").route(web::post().to(reset_tracking_id))) + } +} diff --git a/crates/router/src/routes/blocklist.rs b/crates/router/src/routes/blocklist.rs new file mode 100644 index 000000000000..9c93f49ab83f --- /dev/null +++ b/crates/router/src/routes/blocklist.rs @@ -0,0 +1,119 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::blocklist as api_blocklist; +use router_env::Flow; + +use crate::{ + core::{api_locking, blocklist}, + routes::AppState, + services::{api, authentication as auth, authorization::permissions::Permission}, +}; + +#[utoipa::path( + post, + path = "/blocklist", + request_body = BlocklistRequest, + responses( + (status = 200, description = "Fingerprint Blocked", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "Block a Fingerprint", + security(("api_key" = [])) +)] +pub async fn add_entry_to_blocklist( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::AddToBlocklist; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, body| { + blocklist::add_entry_to_blocklist(state, auth.merchant_account, body) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountWrite), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[utoipa::path( + delete, + path = "/blocklist", + request_body = BlocklistRequest, + responses( + (status = 200, description = "Fingerprint Unblocked", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "Unblock a Fingerprint", + security(("api_key" = [])) +)] +pub async fn remove_entry_from_blocklist( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::DeleteFromBlocklist; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, body| { + blocklist::remove_entry_from_blocklist(state, auth.merchant_account, body) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountWrite), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[utoipa::path( + get, + path = "/blocklist", + params ( + ("data_kind" = BlocklistDataKind, Query, description = "Kind of the fingerprint list requested"), + ), + responses( + (status = 200, description = "Blocked Fingerprints", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "List Blocked fingerprints of a particular kind", + security(("api_key" = [])) +)] +pub async fn list_blocked_payment_methods( + state: web::Data, + req: HttpRequest, + query_payload: web::Query, +) -> HttpResponse { + let flow = Flow::ListBlocklist; + Box::pin(api::server_wrap( + flow, + state, + &req, + query_payload.into_inner(), + |state, auth: auth::AuthenticationData, query| { + blocklist::list_blocklist_entries(state, auth.merchant_account, query) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountRead), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/connector_onboarding.rs b/crates/router/src/routes/connector_onboarding.rs new file mode 100644 index 000000000000..f5555f5bf9bf --- /dev/null +++ b/crates/router/src/routes/connector_onboarding.rs @@ -0,0 +1,66 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::connector_onboarding as api_types; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, connector_onboarding as core}, + services::{api, authentication as auth, authorization::permissions::Permission}, +}; + +pub async fn get_action_url( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::GetActionUrl; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + core::get_action_url, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn sync_onboarding_status( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::SyncOnboardingStatus; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + core::sync_onboarding_status, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn reset_tracking_id( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::ResetTrackingId; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + core::reset_tracking_id, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} 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 cfc37cbdbb2a..012030df6be9 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -4,25 +4,10 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, customers::*}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::customers, }; -/// Create Customer -/// -/// Create a customer object and store the customer details to be reused for future payments. Incase the customer already exists in the system, this API will respond with the customer details. -#[utoipa::path( - post, - path = "/customers", - request_body = CustomerRequest, - responses( - (status = 200, description = "Customer Created", body = CustomerResponse), - (status = 400, description = "Invalid data") - ), - tag = "Customers", - operation_id = "Create a Customer", - security(("api_key" = [])) -)] #[instrument(skip_all, fields(flow = ?Flow::CustomersCreate))] pub async fn customers_create( state: web::Data, @@ -36,26 +21,16 @@ pub async fn customers_create( &req, json_payload.into_inner(), |state, auth, req| create_customer(state, auth.merchant_account, auth.key_store, req), - &auth::ApiKeyAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::CustomerWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await } -/// Retrieve Customer -/// -/// Retrieve a customer's details. -#[utoipa::path( - get, - path = "/customers/{customer_id}", - params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), - responses( - (status = 200, description = "Customer Retrieved", body = CustomerResponse), - (status = 404, description = "Customer was not found") - ), - tag = "Customers", - operation_id = "Retrieve a Customer", - security(("api_key" = []), ("ephemeral_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::CustomersRetrieve))] pub async fn customers_retrieve( state: web::Data, @@ -68,11 +43,14 @@ pub async fn customers_retrieve( }) .into_inner(); - let auth = + let auth = if auth::is_jwt_auth(req.headers()) { + Box::new(auth::JWTAuth(Permission::CustomerRead)) + } else { match auth::is_ephemeral_auth(req.headers(), &*state.store, &payload.customer_id).await { Ok(auth) => auth, Err(err) => return api::log_and_return_error_response(err), - }; + } + }; api::server_wrap( flow, @@ -86,20 +64,6 @@ pub async fn customers_retrieve( .await } -/// List customers for a merchant -/// -/// To filter and list the customers for a particular merchant id -#[utoipa::path( - post, - path = "/customers/list", - responses( - (status = 200, description = "Customers retrieved", body = Vec), - (status = 400, description = "Invalid Data"), - ), - tag = "Customers List", - operation_id = "List all Customers for a Merchant", - security(("api_key" = [])) -)] #[instrument(skip_all, fields(flow = ?Flow::CustomersList))] pub async fn customers_list(state: web::Data, req: HttpRequest) -> HttpResponse { let flow = Flow::CustomersList; @@ -110,28 +74,16 @@ pub async fn customers_list(state: web::Data, req: HttpRequest) -> Htt &req, (), |state, auth, _| list_customers(state, auth.merchant_account.merchant_id, auth.key_store), - &auth::ApiKeyAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::CustomerRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await } -/// Update Customer -/// -/// Updates the customer's details in a customer object. -#[utoipa::path( - post, - path = "/customers/{customer_id}", - request_body = CustomerRequest, - params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), - responses( - (status = 200, description = "Customer was Updated", body = CustomerResponse), - (status = 404, description = "Customer was not found") - ), - tag = "Customers", - operation_id = "Update a Customer", - security(("api_key" = [])) -)] #[instrument(skip_all, fields(flow = ?Flow::CustomersUpdate))] pub async fn customers_update( state: web::Data, @@ -148,26 +100,16 @@ pub async fn customers_update( &req, json_payload.into_inner(), |state, auth, req| update_customer(state, auth.merchant_account, req, auth.key_store), - &auth::ApiKeyAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::CustomerWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await } -/// Delete Customer -/// -/// Delete a customer record. -#[utoipa::path( - delete, - path = "/customers/{customer_id}", - params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), - responses( - (status = 200, description = "Customer was Deleted", body = CustomerDeleteResponse), - (status = 404, description = "Customer was not found") - ), - tag = "Customers", - operation_id = "Delete a Customer", - security(("api_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::CustomersDelete))] pub async fn customers_delete( state: web::Data, @@ -185,7 +127,11 @@ pub async fn customers_delete( &req, payload, |state, auth, req| delete_customer(state, auth.merchant_account, req, auth.key_store), - &auth::ApiKeyAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::CustomerWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -207,9 +153,18 @@ pub async fn get_customer_mandates( &req, customer_id, |state, auth, req| { - crate::core::mandate::get_customer_mandates(state, auth.merchant_account, req) + crate::core::mandate::get_customer_mandates( + state, + auth.merchant_account, + auth.key_store, + req, + ) }, - &auth::ApiKeyAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MandateRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index aaeb118645db..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 @@ -125,7 +133,11 @@ 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 @@ -158,7 +170,11 @@ 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 @@ -199,7 +215,11 @@ 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 @@ -235,7 +255,11 @@ pub async fn retrieve_dispute_evidence( &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/dummy_connector/errors.rs b/crates/router/src/routes/dummy_connector/errors.rs index 4501df0a0fa4..0b4affa8b619 100644 --- a/crates/router/src/routes/dummy_connector/errors.rs +++ b/crates/router/src/routes/dummy_connector/errors.rs @@ -20,7 +20,7 @@ pub enum DummyConnectorErrors { #[error(error_type = ErrorType::InvalidRequestError, code = "DC_02", message = "Missing required param: {field_name}")] MissingRequiredField { field_name: &'static str }, - #[error(error_type = ErrorType::InvalidRequestError, code = "DC_03", message = "Refund amount exceeds the payment amount")] + #[error(error_type = ErrorType::InvalidRequestError, code = "DC_03", message = "The refund amount exceeds the amount captured")] RefundAmountExceedsPaymentAmount, #[error(error_type = ErrorType::InvalidRequestError, code = "DC_04", message = "Card not supported. Please use test cards")] diff --git a/crates/router/src/routes/files.rs b/crates/router/src/routes/files.rs index bde221ebc161..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; @@ -45,7 +45,11 @@ pub async fn files_create( &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 @@ -83,7 +87,11 @@ pub async fn files_delete( &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 @@ -121,7 +129,11 @@ pub async fn files_retrieve( &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/fraud_check.rs b/crates/router/src/routes/fraud_check.rs new file mode 100644 index 000000000000..d4363a236bb3 --- /dev/null +++ b/crates/router/src/routes/fraud_check.rs @@ -0,0 +1,42 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use common_utils::events::{ApiEventMetric, ApiEventsType}; +use router_env::Flow; + +use crate::{ + core::{api_locking, fraud_check as frm_core}, + services::{self, api}, + types::fraud_check::FraudCheckResponseData, + AppState, +}; + +pub async fn frm_fulfillment( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::FrmFulfillment; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth, req| { + frm_core::frm_fulfillment_core(state, auth.merchant_account, auth.key_store, req) + }, + &services::authentication::ApiKeyAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +impl ApiEventMetric for FraudCheckResponseData { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::FraudCheck) + } +} + +impl ApiEventMetric for frm_core::types::FrmFulfillmentRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::FraudCheck) + } +} diff --git a/crates/router/src/routes/health.rs b/crates/router/src/routes/health.rs index 7c7f29bd1819..2183ab07fed7 100644 --- a/crates/router/src/routes/health.rs +++ b/crates/router/src/routes/health.rs @@ -1,7 +1,14 @@ -use router_env::{instrument, logger, tracing}; - -use crate::routes::metrics; +use actix_web::{web, HttpRequest}; +use api_models::health_check::RouterHealthCheckResponse; +use router_env::{instrument, logger, tracing, Flow}; +use super::app; +use crate::{ + core::{api_locking, health_check::HealthCheckInterface}, + errors::{self, RouterResponse}, + routes::metrics, + services::{api, authentication as auth}, +}; /// . // #[logger::instrument(skip_all, name = "name1", level = "warn", fields( key1 = "val1" ))] #[instrument(skip_all)] @@ -11,3 +18,103 @@ pub async fn health() -> impl actix_web::Responder { logger::info!("Health was called"); actix_web::HttpResponse::Ok().body("health is good") } + +#[instrument(skip_all, fields(flow = ?Flow::DeepHealthCheck))] +pub async fn deep_health_check( + state: web::Data, + request: HttpRequest, +) -> impl actix_web::Responder { + metrics::HEALTH_METRIC.add(&metrics::CONTEXT, 1, &[]); + + let flow = Flow::DeepHealthCheck; + + Box::pin(api::server_wrap( + flow, + state, + &request, + (), + |state, _, _| deep_health_check_func(state), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +async fn deep_health_check_func(state: app::AppState) -> RouterResponse { + logger::info!("Deep health check was called"); + + logger::debug!("Database health check begin"); + + let db_status = state.health_check_db().await.map(|_| true).map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Database", + message: err.to_string() + }) + })?; + + logger::debug!("Database health check end"); + + logger::debug!("Redis health check begin"); + + let redis_status = state + .health_check_redis() + .await + .map(|_| true) + .map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Redis", + message: err.to_string() + }) + })?; + + logger::debug!("Redis health check end"); + + logger::debug!("Locker health check begin"); + + let locker_status = state + .health_check_locker() + .await + .map(|_| true) + .map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Locker", + message: err.to_string() + }) + })?; + + #[cfg(feature = "olap")] + let analytics_status = state + .health_check_analytics() + .await + .map(|_| true) + .map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Analytics", + message: err.to_string() + }) + })?; + + let outgoing_check = state + .health_check_outgoing() + .await + .map(|_| true) + .map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Outgoing Request", + message: err.to_string() + }) + })?; + + logger::debug!("Locker health check end"); + + let response = RouterHealthCheckResponse { + database: db_status, + redis: redis_status, + locker: locker_status, + #[cfg(feature = "olap")] + analytics: analytics_status, + outgoing_request: outgoing_check, + }; + + Ok(api::ApplicationResponse::Json(response)) +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index c093523d455a..0dfc3b1b3391 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -11,8 +11,10 @@ pub enum ApiIdentifier { Configs, Customers, Ephemeral, + Health, Mandates, PaymentMethods, + PaymentMethodAuth, Payouts, Disputes, CardsInfo, @@ -23,9 +25,14 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + Blocklist, + Forex, RustLockerMigration, Gsm, User, + UserRole, + ConnectorOnboarding, + Recon, } impl From for ApiIdentifier { @@ -46,7 +53,16 @@ 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::AddToBlocklist => Self::Blocklist, + Flow::DeleteFromBlocklist => Self::Blocklist, + Flow::ListBlocklist => Self::Blocklist, Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve @@ -68,6 +84,7 @@ impl From for ApiIdentifier { Flow::EphemeralKeyCreate | Flow::EphemeralKeyDelete => Self::Ephemeral, + Flow::DeepHealthCheck => Self::Health, Flow::MandatesRetrieve | Flow::MandatesRevoke | Flow::MandatesList => Self::Mandates, Flow::PaymentMethodsCreate @@ -78,6 +95,8 @@ impl From for ApiIdentifier { | Flow::PaymentMethodsDelete | Flow::ValidatePaymentMethod => Self::PaymentMethods, + Flow::PmAuthLinkTokenCreate | Flow::PmAuthExchangeToken => Self::PaymentMethodAuth, + Flow::PaymentsCreate | Flow::PaymentsRetrieve | Flow::PaymentsUpdate @@ -89,7 +108,8 @@ impl From for ApiIdentifier { | Flow::PaymentsSessionToken | Flow::PaymentsStart | Flow::PaymentsList - | Flow::PaymentsRedirect => Self::Payments, + | Flow::PaymentsRedirect + | Flow::PaymentsIncrementalAuthorization => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve @@ -103,7 +123,7 @@ impl From for ApiIdentifier { | Flow::RefundsUpdate | Flow::RefundsList => Self::Refunds, - Flow::IncomingWebhookReceive => Self::Webhooks, + Flow::FrmFulfillment | Flow::IncomingWebhookReceive => Self::Webhooks, Flow::ApiKeyCreate | Flow::ApiKeyRetrieve @@ -129,16 +149,61 @@ impl From for ApiIdentifier { | Flow::BusinessProfileDelete | Flow::BusinessProfileList => Self::Business, + Flow::PaymentLinkRetrieve + | Flow::PaymentLinkInitiate + | Flow::PaymentLinkList + | Flow::PaymentLinkStatus => 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::UserSignUp + | Flow::UserSignInWithoutInviteChecks + | Flow::UserSignIn + | Flow::Signout + | Flow::ChangePassword + | Flow::SetDashboardMetadata + | Flow::GetMutltipleDashboardMetadata + | Flow::VerifyPaymentConnector + | Flow::InternalUserSignup + | Flow::SwitchMerchant + | Flow::UserMerchantAccountCreate + | Flow::GenerateSampleData + | Flow::DeleteSampleData + | Flow::UserMerchantAccountList + | Flow::GetUserDetails + | Flow::ForgotPassword + | Flow::ResetPassword + | Flow::InviteUser + | Flow::InviteMultipleUser + | Flow::UserSignUpWithMerchantId + | Flow::VerifyEmailWithoutInviteChecks + | Flow::VerifyEmail + | Flow::VerifyEmailRequest + | Flow::UpdateUserAccountDetails => Self::User, + + Flow::ListRoles + | Flow::GetRole + | Flow::GetRoleFromToken + | Flow::UpdateUserRole + | Flow::GetAuthorizationInfo + | Flow::AcceptInvitation + | Flow::DeleteUserRole => Self::UserRole, + + Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { + Self::ConnectorOnboarding + } + + Flow::ReconMerchantUpdate + | Flow::ReconTokenRequest + | Flow::ReconServiceRequest + | Flow::ReconVerifyToken => Self::Recon, } } } diff --git a/crates/router/src/routes/locker_migration.rs b/crates/router/src/routes/locker_migration.rs index 892dc5941bd6..a3df0c3a229b 100644 --- a/crates/router/src/routes/locker_migration.rs +++ b/crates/router/src/routes/locker_migration.rs @@ -14,7 +14,7 @@ pub async fn rust_locker_migration( ) -> HttpResponse { let flow = Flow::RustLockerMigration; let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -22,6 +22,6 @@ pub async fn rust_locker_migration( |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..eafd61dcded8 100644 --- a/crates/router/src/routes/mandates.rs +++ b/crates/router/src/routes/mandates.rs @@ -4,13 +4,13 @@ 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, }; /// Mandates - Retrieve Mandate /// -/// Retrieve a mandate +/// Retrieves a mandate created using the Payments/Create API #[utoipa::path( get, path = "/mandates/{mandate_id}", @@ -41,7 +41,7 @@ pub async fn get_mandate( state, &req, mandate_id, - |state, auth, req| mandate::get_mandate(state, auth.merchant_account, req), + |state, auth, req| mandate::get_mandate(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, ) @@ -49,12 +49,12 @@ pub async fn get_mandate( } /// Mandates - Revoke Mandate /// -/// Revoke a mandate +/// Revokes a mandate created using the Payments/Create API #[utoipa::path( post, path = "/mandates/revoke/{mandate_id}", params( - ("mandate_id" = String, Path, description = "The identifier for mandate") + ("mandate_id" = String, Path, description = "The identifier for a mandate") ), responses( (status = 200, description = "The mandate was revoked successfully", body = MandateRevokedResponse), @@ -75,15 +75,17 @@ pub async fn revoke_mandate( let mandate_id = mandates::MandateId { mandate_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, mandate_id, - |state, auth, req| mandate::revoke_mandate(state, auth.merchant_account, req), + |state, auth, req| { + mandate::revoke_mandate(state, auth.merchant_account, auth.key_store, req) + }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Mandates - List Mandates @@ -121,8 +123,14 @@ pub async fn retrieve_mandates_list( state, &req, payload, - |state, auth, req| mandate::retrieve_mandates_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + |state, auth, req| { + mandate::retrieve_mandates_list(state, auth.merchant_account, auth.key_store, req) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MandateRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index a8e6f9d2a892..6c3293dba9d0 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -6,7 +6,9 @@ global_meter!(GLOBAL_METER, "ROUTER_API"); counter_metric!(HEALTH_METRIC, GLOBAL_METER); // No. of health API hits counter_metric!(KV_MISS, GLOBAL_METER); // No. of KV misses #[cfg(feature = "kms")] -counter_metric!(AWS_KMS_FAILURES, GLOBAL_METER); // No. of AWS KMS API failures +counter_metric!(AWS_KMS_ENCRYPTION_FAILURES, GLOBAL_METER); // No. of AWS KMS Encryption failures +#[cfg(feature = "kms")] +counter_metric!(AWS_KMS_DECRYPTION_FAILURES, GLOBAL_METER); // No. of AWS KMS Decryption failures // API Level Metrics counter_metric!(REQUESTS_RECEIVED, GLOBAL_METER); @@ -85,6 +87,7 @@ counter_metric!(CONNECTOR_HTTP_STATUS_CODE_5XX_COUNT, GLOBAL_METER); // Service Level counter_metric!(CARD_LOCKER_FAILURES, GLOBAL_METER); +counter_metric!(CARD_LOCKER_SUCCESSFUL_RESPONSE, GLOBAL_METER); counter_metric!(TEMP_LOCKER_FAILURES, GLOBAL_METER); histogram_metric!(CARD_ADD_TIME, GLOBAL_METER); histogram_metric!(CARD_GET_TIME, GLOBAL_METER); @@ -110,5 +113,8 @@ counter_metric!(AUTO_RETRY_GSM_MATCH_COUNT, GLOBAL_METER); counter_metric!(AUTO_RETRY_EXHAUSTED_COUNT, GLOBAL_METER); counter_metric!(AUTO_RETRY_PAYMENT_COUNT, GLOBAL_METER); +counter_metric!(TASKS_ADDED_COUNT, GLOBAL_METER); // Tasks added to process tracker +counter_metric!(TASKS_RESET_COUNT, GLOBAL_METER); // Tasks reset in process tracker for requeue flow + pub mod request; pub mod utils; diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index 7d6bf1a05f09..4d79b9231637 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -80,3 +80,76 @@ pub async fn initiate_payment_link( )) .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 +} + +pub async fn payment_link_status( + state: web::Data, + req: actix_web::HttpRequest, + path: web::Path<(String, String)>, +) -> impl Responder { + let flow = Flow::PaymentLinkStatus; + let (merchant_id, payment_id) = path.into_inner(); + let payload = api_models::payments::PaymentLinkInitiateRequest { + payment_id, + merchant_id: merchant_id.clone(), + }; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload.clone(), + |state, auth, _| { + get_payment_link_status( + state, + auth.merchant_account, + payload.merchant_id.clone(), + payload.payment_id.clone(), + ) + }, + &crate::services::authentication::MerchantIdAuth(merchant_id), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 83d4c7f96611..ef7047bffabc 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -9,24 +9,13 @@ 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 -/// -/// To create a payment method against a customer object. In case of cards, this API could be used only by PCI compliant merchants -#[utoipa::path( - post, - path = "/payment_methods", - request_body = PaymentMethodCreate, - responses( - (status = 200, description = "Payment Method Created", body = PaymentMethodResponse), - (status = 400, description = "Invalid Data") - ), - tag = "Payment Methods", - operation_id = "Create a Payment Method", - security(("api_key" = [])) -)] #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsCreate))] pub async fn create_payment_method_api( state: web::Data, @@ -40,37 +29,20 @@ pub async fn create_payment_method_api( &req, json_payload.into_inner(), |state, auth, req| async move { - cards::add_payment_method(state, req, &auth.merchant_account, &auth.key_store).await + Box::pin(cards::add_payment_method( + state, + req, + &auth.merchant_account, + &auth.key_store, + )) + .await }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, )) .await } -/// List payment methods for a Merchant -/// -/// To filter and list the applicable payment methods for a particular Merchant ID -#[utoipa::path( - get, - path = "/account/payment_methods", - params ( - ("account_id" = String, Path, description = "The unique identifier for the merchant account"), - ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), - ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), - ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), - ("maximum_amount" = i64, Query, description = "The maximum amount amount accepted for processing by the particular payment method."), - ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), - ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), - ), - responses( - (status = 200, description = "Payment Methods retrieved", body = PaymentMethodListResponse), - (status = 400, description = "Invalid Data"), - (status = 404, description = "Payment Methods does not exist in records") - ), - tag = "Payment Methods", - operation_id = "List all Payment Methods for a Merchant", - security(("api_key" = []), ("publishable_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsList))] pub async fn list_payment_method_api( state: web::Data, @@ -104,7 +76,6 @@ pub async fn list_payment_method_api( get, path = "/customers/{customer_id}/payment_methods", params ( - ("customer_id" = String, Path, description = "The unique identifier for the customer account"), ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), @@ -130,10 +101,6 @@ pub async fn list_customer_payment_method_api( ) -> HttpResponse { let flow = Flow::CustomerPaymentMethodsList; let payload = query_payload.into_inner(); - let (auth, _) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { - Ok((auth, _auth_flow)) => (auth, _auth_flow), - Err(e) => return api::log_and_return_error_response(e), - }; let customer_id = customer_id.into_inner().0; Box::pin(api::server_wrap( flow, @@ -149,7 +116,7 @@ pub async fn list_customer_payment_method_api( Some(&customer_id), ) }, - &*auth, + &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, )) .await @@ -162,7 +129,6 @@ pub async fn list_customer_payment_method_api( path = "/customers/payment_methods", params ( ("client-secret" = String, Path, description = "A secret known only to your application and the authorization server"), - ("customer_id" = String, Path, description = "The unique identifier for the customer account"), ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), @@ -210,23 +176,7 @@ pub async fn list_customer_payment_method_api_client( )) .await } -/// Payment Method - Retrieve -/// -/// To retrieve a payment method -#[utoipa::path( - get, - path = "/payment_methods/{method_id}", - params ( - ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), - ), - responses( - (status = 200, description = "Payment Method retrieved", body = PaymentMethodResponse), - (status = 404, description = "Payment Method does not exist in records") - ), - tag = "Payment Methods", - operation_id = "Retrieve a Payment method", - security(("api_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsRetrieve))] pub async fn payment_method_retrieve_api( state: web::Data, @@ -244,30 +194,13 @@ pub async fn payment_method_retrieve_api( state, &req, payload, - |state, _auth, pm| cards::retrieve_payment_method(state, pm), + |state, auth, pm| cards::retrieve_payment_method(state, pm, auth.key_store), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, )) .await } -/// Payment Method - Update -/// -/// To update an existing payment method attached to a customer object. This API is useful for use cases such as updating the card number for expired cards to prevent discontinuity in recurring payments -#[utoipa::path( - post, - path = "/payment_methods/{method_id}", - params ( - ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), - ), - request_body = PaymentMethodUpdate, - responses( - (status = 200, description = "Payment Method updated", body = PaymentMethodResponse), - (status = 404, description = "Payment Method does not exist in records") - ), - tag = "Payment Methods", - operation_id = "Update a Payment method", - security(("api_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsUpdate))] pub async fn payment_method_update_api( state: web::Data, @@ -297,23 +230,7 @@ pub async fn payment_method_update_api( )) .await } -/// Payment Method - Delete -/// -/// Delete payment method -#[utoipa::path( - delete, - path = "/payment_methods/{method_id}", - params ( - ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), - ), - responses( - (status = 200, description = "Payment Method deleted", body = PaymentMethodDeleteResponse), - (status = 404, description = "Payment Method does not exist in records") - ), - tag = "Payment Methods", - operation_id = "Delete a Payment method", - security(("api_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsDelete))] pub async fn payment_method_delete_api( state: web::Data, @@ -379,9 +296,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 +312,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 b05fae65338a..b822e6d4e686 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -1,28 +1,29 @@ -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, - // PAYMENTS_CREATE_WITH_CUSTOMER_DATA, PAYMENTS_CREATE_WITH_FORCED_3DS, - // PAYMENTS_CREATE_WITH_MANUAL_CAPTURE, PAYMENTS_CREATE_WITH_NOON_ORDER_CATETORY, - // PAYMENTS_CREATE_WITH_ORDER_DETAILS, - // }, 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, }, @@ -87,19 +88,34 @@ use crate::{ operation_id = "Create a Payment", security(("api_key" = [])), )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsCreate))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsCreate, payment_id))] pub async fn payments_create( state: web::Data, req: actix_web::HttpRequest, 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); + } + + tracing::Span::current().record( + "payment_id", + &payload + .payment_id + .as_ref() + .map(|payment_id_type| payment_id_type.get_payment_intent_id()) + .transpose() + .unwrap_or_default() + .unwrap_or_default(), + ); + let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -120,7 +136,11 @@ 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, )) @@ -144,7 +164,7 @@ pub async fn payments_create( // tag = "Payments", // operation_id = "Start a Redirection Payment" // )] -#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsStart))] +#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsStart, payment_id))] pub async fn payments_start( state: web::Data, req: actix_web::HttpRequest, @@ -159,6 +179,7 @@ pub async fn payments_start( }; let locking_action = payload.get_locking_input(flow.clone()); + tracing::Span::current().record("payment_id", &payment_id); Box::pin(api::server_wrap( flow, @@ -208,7 +229,7 @@ pub async fn payments_start( operation_id = "Retrieve a Payment", security(("api_key" = []), ("publishable_key" = [])) )] -#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsRetrieve))] +#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsRetrieve, payment_id))] // #[get("/{payment_id}")] pub async fn payments_retrieve( state: web::Data, @@ -226,6 +247,9 @@ pub async fn payments_retrieve( expand_captures: json_payload.expand_captures, ..Default::default() }; + + tracing::Span::current().record("payment_id", &path.to_string()); + let (auth_type, auth_flow) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { Ok(auth) => auth, @@ -254,7 +278,7 @@ pub async fn payments_retrieve( }, auth::auth_type( &*auth_type, - &auth::JWTAuth, + &auth::JWTAuth(Permission::PaymentRead), req.headers(), ), locking_action, @@ -276,7 +300,7 @@ pub async fn payments_retrieve( operation_id = "Retrieve a Payment", security(("api_key" = [])) )] -#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsRetrieve))] +#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsRetrieve, payment_id))] // #[post("/sync")] pub async fn payments_retrieve_with_gateway_creds( state: web::Data, @@ -298,6 +322,8 @@ pub async fn payments_retrieve_with_gateway_creds( }; let flow = Flow::PaymentsRetrieve; + tracing::Span::current().record("payment_id", &json_payload.payment_id); + let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -341,7 +367,7 @@ pub async fn payments_retrieve_with_gateway_creds( operation_id = "Update a Payment", security(("api_key" = []), ("publishable_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsUpdate))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsUpdate, payment_id))] // #[post("/{payment_id}")] pub async fn payments_update( state: web::Data, @@ -358,6 +384,8 @@ pub async fn payments_update( let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = Some(payment_types::PaymentIdType::PaymentIntentId(payment_id)); let (auth_type, auth_flow) = match auth::get_auth_type_and_flow(req.headers()) { @@ -406,7 +434,7 @@ pub async fn payments_update( operation_id = "Confirm a Payment", security(("api_key" = []), ("publishable_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsConfirm))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsConfirm, payment_id))] // #[post("/{payment_id}/confirm")] pub async fn payments_confirm( state: web::Data, @@ -426,6 +454,7 @@ pub async fn payments_confirm( } let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); payload.payment_id = Some(payment_types::PaymentIdType::PaymentIntentId(payment_id)); payload.confirm = Some(true); let header_payload = match payment_types::HeaderPayload::foreign_try_from(req.headers()) { @@ -482,7 +511,7 @@ pub async fn payments_confirm( operation_id = "Capture a Payment", security(("api_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsCapture))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsCapture, payment_id))] // #[post("/{payment_id}/capture")] pub async fn payments_capture( state: web::Data, @@ -490,9 +519,12 @@ pub async fn payments_capture( json_payload: web::Json, path: web::Path, ) -> impl Responder { + let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + let flow = Flow::PaymentsCapture; let payload = payment_types::PaymentsCaptureRequest { - payment_id: path.into_inner(), + payment_id, ..json_payload.into_inner() }; @@ -543,7 +575,7 @@ pub async fn payments_capture( operation_id = "Create Session tokens for a Payment", security(("publishable_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsSessionToken))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsSessionToken, payment_id))] pub async fn payments_connector_session( state: web::Data, req: actix_web::HttpRequest, @@ -552,6 +584,8 @@ pub async fn payments_connector_session( let flow = Flow::PaymentsSessionToken; let payload = json_payload.into_inner(); + tracing::Span::current().record("payment_id", &payload.payment_id); + let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -602,7 +636,7 @@ pub async fn payments_connector_session( // tag = "Payments", // operation_id = "Get Redirect Response for a Payment" // )] -#[instrument(skip_all)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsRedirect, payment_id))] pub async fn payments_redirect_response( state: web::Data, req: actix_web::HttpRequest, @@ -613,6 +647,8 @@ pub async fn payments_redirect_response( let (payment_id, merchant_id, connector) = path.into_inner(); let param_string = req.query_string(); + tracing::Span::current().record("payment_id", &payment_id); + let payload = payments::PaymentsRedirectResponseData { resource_id: payment_types::PaymentIdType::PaymentIntentId(payment_id), merchant_id: Some(merchant_id.clone()), @@ -661,7 +697,7 @@ pub async fn payments_redirect_response( // tag = "Payments", // operation_id = "Get Redirect Response for a Payment" // )] -#[instrument(skip_all)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsRedirect, payment_id))] pub async fn payments_redirect_response_with_creds_identifier( state: web::Data, req: actix_web::HttpRequest, @@ -670,6 +706,8 @@ pub async fn payments_redirect_response_with_creds_identifier( let (payment_id, merchant_id, connector, creds_identifier) = path.into_inner(); let param_string = req.query_string(); + tracing::Span::current().record("payment_id", &payment_id); + let payload = payments::PaymentsRedirectResponseData { resource_id: payment_types::PaymentIdType::PaymentIntentId(payment_id), merchant_id: Some(merchant_id.clone()), @@ -700,7 +738,7 @@ pub async fn payments_redirect_response_with_creds_identifier( ) .await } -#[instrument(skip_all)] +#[instrument(skip_all, fields(flow =? Flow::PaymentsRedirect, payment_id))] pub async fn payments_complete_authorize( state: web::Data, req: actix_web::HttpRequest, @@ -711,6 +749,8 @@ pub async fn payments_complete_authorize( let (payment_id, merchant_id, connector) = path.into_inner(); let param_string = req.query_string(); + tracing::Span::current().record("payment_id", &payment_id); + let payload = payments::PaymentsRedirectResponseData { resource_id: payment_types::PaymentIdType::PaymentIntentId(payment_id), merchant_id: Some(merchant_id.clone()), @@ -759,7 +799,7 @@ pub async fn payments_complete_authorize( operation_id = "Cancel a Payment", security(("api_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsCancel))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsCancel, payment_id))] // #[post("/{payment_id}/cancel")] pub async fn payments_cancel( state: web::Data, @@ -770,6 +810,9 @@ pub async fn payments_cancel( let flow = Flow::PaymentsCancel; let mut payload = json_payload.into_inner(); let payment_id = path.into_inner(); + + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = payment_id; let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -835,7 +878,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 +902,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,11 +926,137 @@ 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 } + +#[cfg(feature = "oltp")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsApprove, payment_id))] +// #[post("/{payment_id}/approve")] +pub async fn payments_approve( + state: web::Data, + http_req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + + tracing::Span::current().record("payment_id", &payment_id); + + payload.payment_id = payment_id; + let flow = Flow::PaymentsApprove; + let fpayload = FPaymentsApproveRequest(&payload); + let locking_action = fpayload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + payload.clone(), + |state, auth, req| { + payments::payments_core::< + api_types::Capture, + payment_types::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + auth.merchant_account, + auth.key_store, + payments::PaymentApprove, + payment_types::PaymentsCaptureRequest { + payment_id: req.payment_id, + ..Default::default() + }, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + payment_types::HeaderPayload::default(), + ) + }, + match env::which() { + env::Env::Production => &auth::ApiKeyAuth, + _ => auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentWrite), + http_req.headers(), + ), + }, + locking_action, + )) + .await +} + +#[cfg(feature = "oltp")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsReject, payment_id))] +// #[post("/{payment_id}/reject")] +pub async fn payments_reject( + state: web::Data, + http_req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + + tracing::Span::current().record("payment_id", &payment_id); + + payload.payment_id = payment_id; + let flow = Flow::PaymentsReject; + let fpayload = FPaymentsRejectRequest(&payload); + let locking_action = fpayload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + payload.clone(), + |state, auth, req| { + payments::payments_core::< + api_types::Void, + payment_types::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + auth.merchant_account, + auth.key_store, + payments::PaymentReject, + payment_types::PaymentsCancelRequest { + payment_id: req.payment_id, + cancellation_reason: Some("Rejected by merchant".to_string()), + ..Default::default() + }, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + payment_types::HeaderPayload::default(), + ) + }, + match env::which() { + env::Env::Production => &auth::ApiKeyAuth, + _ => auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentWrite), + http_req.headers(), + ), + }, + locking_action, + )) + .await +} + async fn authorize_verify_select( operation: Op, state: app::AppState, @@ -959,6 +1136,93 @@ where } } +/// Payments - Incremental Authorization +/// +/// Authorized amount for a payment can be incremented if it is in status: requires_capture +#[utoipa::path( + post, + path = "/payments/{payment_id}/incremental_authorization", + request_body=PaymentsIncrementalAuthorizationRequest, + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + responses( + (status = 200, description = "Payment authorized amount incremented", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Increment authorized amount for a Payment", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsIncrementalAuthorization, payment_id))] +pub async fn payments_incremental_authorization( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentsIncrementalAuthorization; + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + + tracing::Span::current().record("payment_id", &payment_id); + + payload.payment_id = payment_id; + let locking_action = payload.get_locking_input(flow.clone()); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, req| { + payments::payments_core::< + api_types::IncrementalAuthorization, + payment_types::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + auth.merchant_account, + auth.key_store, + payments::PaymentIncrementalAuthorization, + req, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + HeaderPayload::default(), + ) + }, + &auth::ApiKeyAuth, + locking_action, + )) + .await +} + +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 @@ -1003,7 +1267,7 @@ impl GetLockingInput for payment_types::PaymentsRetrieveRequest { lock_utils::ApiIdentifier: From, { match self.resource_id { - payment_types::PaymentIdType::PaymentIntentId(ref id) => { + payment_types::PaymentIdType::PaymentIntentId(ref id) if self.force_sync => { api_locking::LockAction::Hold { input: api_locking::LockingInput { unique_locking_key: id.to_owned(), @@ -1085,3 +1349,55 @@ impl GetLockingInput for payment_types::PaymentsCaptureRequest { } } } + +struct FPaymentsApproveRequest<'a>(&'a payment_types::PaymentsApproveRequest); + +impl<'a> GetLockingInput for FPaymentsApproveRequest<'a> { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.0.payment_id.to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} + +struct FPaymentsRejectRequest<'a>(&'a payment_types::PaymentsRejectRequest); + +impl<'a> GetLockingInput for FPaymentsRejectRequest<'a> { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.0.payment_id.to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} + +impl GetLockingInput for payment_types::PaymentsIncrementalAuthorizationRequest { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.payment_id.to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} diff --git a/crates/router/src/routes/pm_auth.rs b/crates/router/src/routes/pm_auth.rs new file mode 100644 index 000000000000..cfadd787c310 --- /dev/null +++ b/crates/router/src/routes/pm_auth.rs @@ -0,0 +1,73 @@ +use actix_web::{web, HttpRequest, Responder}; +use api_models as api_types; +use router_env::{instrument, tracing, types::Flow}; + +use crate::{core::api_locking, routes::AppState, services::api as oss_api}; + +#[instrument(skip_all, fields(flow = ?Flow::PmAuthLinkTokenCreate))] +pub async fn link_token_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + let flow = Flow::PmAuthLinkTokenCreate; + let (auth, _) = match crate::services::authentication::check_client_secret_and_get_auth( + req.headers(), + &payload, + ) { + Ok((auth, _auth_flow)) => (auth, _auth_flow), + Err(e) => return oss_api::log_and_return_error_response(e), + }; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, payload| { + crate::core::pm_auth::create_link_token( + state, + auth.merchant_account, + auth.key_store, + payload, + ) + }, + &*auth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::PmAuthExchangeToken))] +pub async fn exchange_token( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + let flow = Flow::PmAuthExchangeToken; + let (auth, _) = match crate::services::authentication::check_client_secret_and_get_auth( + req.headers(), + &payload, + ) { + Ok((auth, _auth_flow)) => (auth, _auth_flow), + Err(e) => return oss_api::log_and_return_error_response(e), + }; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, payload| { + crate::core::pm_auth::exchange_token_core( + state, + auth.merchant_account, + auth.key_store, + payload, + ) + }, + &*auth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/recon.rs b/crates/router/src/routes/recon.rs new file mode 100644 index 000000000000..22c886e13581 --- /dev/null +++ b/crates/router/src/routes/recon.rs @@ -0,0 +1,250 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::recon as recon_api; +use common_enums::ReconStatus; +use error_stack::ResultExt; +use masking::{ExposeInterface, PeekInterface, Secret}; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{ + api_locking, + errors::{self, RouterResponse, RouterResult, StorageErrorExt, UserErrors}, + }, + services::{ + api as service_api, api, + authentication::{self as auth, ReconUser, UserFromToken}, + email::types as email_types, + recon::ReconToken, + }, + types::{ + api::{self as api_types, enums}, + domain::{UserEmail, UserFromStorage, UserName}, + storage, + }, +}; + +pub async fn update_merchant( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::ReconMerchantUpdate; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, _user, req| recon_merchant_account_update(state, req), + &auth::ReconAdmin, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn request_for_recon(state: web::Data, http_req: HttpRequest) -> HttpResponse { + let flow = Flow::ReconServiceRequest; + Box::pin(api::server_wrap( + flow, + state, + &http_req, + (), + |state, user: UserFromToken, _req| send_recon_request(state, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_recon_token(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::ReconTokenRequest; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, user: ReconUser, _| generate_recon_token(state, user), + &auth::ReconJWT, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn send_recon_request( + state: AppState, + user: UserFromToken, +) -> RouterResponse { + let db = &*state.store; + let user_from_db = db + .find_user_by_id(&user.user_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let merchant_id = db + .find_user_role_by_user_id(&user.user_id) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)? + .merchant_id; + let key_store = db + .get_merchant_key_store_by_merchant_id( + merchant_id.as_str(), + &db.get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + let merchant_account = db + .find_merchant_account_by_merchant_id(merchant_id.as_str(), &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let email_contents = email_types::ProFeatureRequest { + feature_name: "RECONCILIATION & SETTLEMENT".to_string(), + merchant_id: merchant_id.clone(), + user_name: UserName::new(user_from_db.name) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to form username")?, + recipient_email: UserEmail::new(Secret::new("biz@hyperswitch.io".to_string())) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert recipient's email to UserEmail")?, + settings: state.conf.clone(), + subject: format!( + "Dashboard Pro Feature Request by {}", + user_from_db.email.expose().peek() + ), + }; + + let is_email_sent = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to compose and send email for ProFeatureRequest") + .is_ok(); + + if is_email_sent { + let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate { + recon_status: enums::ReconStatus::Requested, + }; + + let response = db + .update_merchant(merchant_account, updated_merchant_account, &key_store) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!("Failed while updating merchant's recon status: {merchant_id}") + })?; + + Ok(service_api::ApplicationResponse::Json( + recon_api::ReconStatusResponse { + recon_status: response.recon_status, + }, + )) + } else { + Ok(service_api::ApplicationResponse::Json( + recon_api::ReconStatusResponse { + recon_status: enums::ReconStatus::NotRequested, + }, + )) + } +} + +pub async fn recon_merchant_account_update( + state: AppState, + req: recon_api::ReconUpdateMerchantRequest, +) -> RouterResponse { + let merchant_id = &req.merchant_id.clone(); + let user_email = &req.user_email.clone(); + + let db = &*state.store; + + let key_store = db + .get_merchant_key_store_by_merchant_id( + &req.merchant_id, + &db.get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let merchant_account = db + .find_merchant_account_by_merchant_id(merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate { + recon_status: req.recon_status, + }; + + let response = db + .update_merchant(merchant_account, updated_merchant_account, &key_store) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!("Failed while updating merchant's recon status: {merchant_id}") + })?; + + let email_contents = email_types::ReconActivation { + recipient_email: UserEmail::from_pii_email(user_email.clone()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert recipient's email to UserEmail from pii::Email")?, + user_name: UserName::new(Secret::new("HyperSwitch User".to_string())) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to form username")?, + settings: state.conf.clone(), + subject: "Approval of Recon Request - Access Granted to Recon Dashboard", + }; + + if req.recon_status == ReconStatus::Active { + let _is_email_sent = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to compose and send email for ReconActivation") + .is_ok(); + } + + Ok(service_api::ApplicationResponse::Json( + response + .try_into() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_account", + })?, + )) +} + +pub async fn generate_recon_token( + state: AppState, + req: ReconUser, +) -> RouterResponse { + let db = &*state.store; + let user = db + .find_user_by_id(&req.user_id) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(errors::ApiErrorResponse::InvalidJwtToken) + } else { + e.change_context(errors::ApiErrorResponse::InternalServerError) + } + })? + .into(); + + let token = Box::pin(get_recon_auth_token(user, state)) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + Ok(service_api::ApplicationResponse::Json( + recon_api::ReconTokenResponse { token }, + )) +} + +pub async fn get_recon_auth_token( + user: UserFromStorage, + state: AppState, +) -> RouterResult> { + ReconToken::new_token(user.0.user_id.clone(), &state.conf).await +} diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index d370af6b8d7a..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, }; @@ -37,7 +37,11 @@ pub async fn refunds_create( &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 @@ -88,7 +92,11 @@ 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 @@ -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..0f139e936146 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 @@ -101,7 +113,7 @@ pub async fn routing_retrieve_config( #[cfg(feature = "olap")] #[instrument(skip_all)] -pub async fn routing_retrieve_dictionary( +pub async fn list_routing_configs( state: web::Data, req: HttpRequest, #[cfg(feature = "business_profile_routing")] query: web::Query, @@ -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..0c2694dc70f8 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -1,16 +1,102 @@ use actix_web::{web, HttpRequest, HttpResponse}; -use api_models::user as user_api; +#[cfg(feature = "dummy_connector")] +use api_models::user::sample_data::SampleDataRequest; +use api_models::{ + errors::types::ApiErrorResponse, + user::{self 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}, }; +#[cfg(feature = "email")] +pub async fn user_signup_with_merchant_id( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignUpWithMerchantId; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signup_with_merchant_id(state, req_body), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_signup( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignUp; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signup(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_signin_without_invite_checks( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignInWithoutInviteChecks; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signin_without_invite_checks(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_signin( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignIn; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signin(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] pub async fn user_connect_account( state: web::Data, http_req: HttpRequest, @@ -23,9 +109,384 @@ 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 signout(state: web::Data, http_req: HttpRequest) -> HttpResponse { + let flow = Flow::Signout; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + (), + |state, user, _| user_core::signout(state, user), + &auth::DashboardNoPermissionAuth, + 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_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 +} + +#[cfg(feature = "dummy_connector")] +pub async fn generate_sample_data( + state: web::Data, + http_req: HttpRequest, + payload: web::Json, +) -> impl actix_web::Responder { + use crate::core::user::sample_data; + + let flow = Flow::GenerateSampleData; + Box::pin(api::server_wrap( + flow, + state, + &http_req, + payload.into_inner(), + sample_data::generate_sample_data_for_user, + &auth::JWTAuth(Permission::PaymentWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} +#[cfg(feature = "dummy_connector")] +pub async fn delete_sample_data( + state: web::Data, + http_req: HttpRequest, + payload: web::Json, +) -> impl actix_web::Responder { + use crate::core::user::sample_data; + + let flow = Flow::DeleteSampleData; + Box::pin(api::server_wrap( + flow, + state, + &http_req, + payload.into_inner(), + sample_data::delete_sample_data_for_user, + &auth::JWTAuth(Permission::PaymentWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn list_merchant_ids_for_user( + state: web::Data, + req: HttpRequest, +) -> HttpResponse { + let flow = Flow::UserMerchantAccountList; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, user, _| user_core::list_merchant_ids_for_user(state, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_user_details(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::GetUserDetails; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, user, _| user_core::get_users_for_merchant_account(state, user), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] +pub async fn forgot_password( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::ForgotPassword; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + |state, _, payload| user_core::forgot_password(state, payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] +pub async fn reset_password( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::ResetPassword; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + |state, _, payload| user_core::reset_password(state, payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn invite_user( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::InviteUser; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + |state, user, payload| user_core::invite_user(state, payload, user), + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} +pub async fn invite_multiple_user( + state: web::Data, + req: HttpRequest, + payload: web::Json>, +) -> HttpResponse { + let flow = Flow::InviteMultipleUser; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + user_core::invite_multiple_user, + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] +pub async fn verify_email_without_invite_checks( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::VerifyEmailWithoutInviteChecks; + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + json_payload.into_inner(), + |state, _, req_payload| user_core::verify_email_without_invite_checks(state, req_payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] +pub async fn verify_email( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::VerifyEmail; + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + json_payload.into_inner(), + |state, _, req_payload| user_core::verify_email(state, req_payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] +pub async fn verify_email_request( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::VerifyEmailRequest; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, _, req_body| user_core::send_verification_mail(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "recon")] +pub async fn verify_recon_token(state: web::Data, http_req: HttpRequest) -> HttpResponse { + let flow = Flow::ReconVerifyToken; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + (), + |state, user, _req| user_core::verify_token(state, user), + &auth::ReconJWT, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn update_user_account_details( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UpdateUserAccountDetails; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + user_core::update_user_details, + &auth::DashboardNoPermissionAuth, + 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..ec05db1d6150 --- /dev/null +++ b/crates/router/src/routes/user_role.rs @@ -0,0 +1,135 @@ +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, UserFromToken}, + 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_all_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 get_role_from_token(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::GetRoleFromToken; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, user: UserFromToken, _| user_role_core::get_role_from_token(state, user), + &auth::DashboardNoPermissionAuth, + 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 +} + +pub async fn accept_invitation( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::AcceptInvitation; + let payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + user_role_core::accept_invitation, + &auth::UserWithoutMerchantJWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn delete_user_role( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::DeleteUserRole; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + user_role_core::delete_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 d0525bb272e8..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))] @@ -32,7 +32,11 @@ 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 63f2328ec6ce..10eb4ef75e4d 100644 --- a/crates/router/src/routes/webhooks.rs +++ b/crates/router/src/routes/webhooks.rs @@ -22,18 +22,19 @@ pub async fn receive_incoming_webhook( let (merchant_id, connector_id_or_name) = path.into_inner(); Box::pin(api::server_wrap( - flow, + flow.clone(), state, &req, - (), - |state, auth, _| { + WebhookBytes(body), + |state, auth, payload| { webhooks::webhooks_wrapper::( + &flow, state.to_owned(), &req, auth.merchant_account, auth.key_store, &connector_id_or_name, - body.clone(), + payload.0, ) }, &auth::MerchantIdAuth(merchant_id), @@ -41,3 +42,19 @@ pub async fn receive_incoming_webhook( )) .await } + +#[derive(Debug)] +struct WebhookBytes(web::Bytes); + +impl serde::Serialize for WebhookBytes { + fn serialize(&self, serializer: S) -> Result { + let payload: serde_json::Value = serde_json::from_slice(&self.0).unwrap_or_default(); + payload.serialize(serializer) + } +} + +impl common_utils::events::ApiEventMetric for WebhookBytes { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Miscellaneous) + } +} diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 21f33f0fa0b8..c0ed2b442d0e 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -1,19 +1,27 @@ pub mod api; pub mod authentication; +pub mod authorization; pub mod encryption; #[cfg(feature = "olap")] pub mod jwt; +pub mod kafka; pub mod logger; +pub mod pm_auth; +#[cfg(feature = "recon")] +pub mod recon; -#[cfg(feature = "kms")] +#[cfg(feature = "email")] +pub mod email; + +#[cfg(any(feature = "kms", feature = "hashicorp-vault"))] use data_models::errors::StorageError; use data_models::errors::StorageResult; use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; -#[cfg(not(feature = "kms"))] -use masking::PeekInterface; -use masking::StrongSecret; +use masking::{PeekInterface, StrongSecret}; #[cfg(feature = "kv_store")] use storage_impl::KVRouterStore; use storage_impl::RouterStore; @@ -40,39 +48,58 @@ pub async fn get_store( #[cfg(feature = "kms")] let kms_client = kms::get_kms_client(&config.kms).await; + #[cfg(feature = "hashicorp-vault")] + let hc_client = external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + .await + .change_context(StorageError::InitializationError)?; + + let master_config = config.master_database.clone(); + + #[cfg(feature = "hashicorp-vault")] + let master_config = master_config + .fetch_inner::(hc_client) + .await + .change_context(StorageError::InitializationError) + .attach_printable("Failed to fetch data from hashicorp vault")?; + #[cfg(feature = "kms")] - let master_config = config - .master_database - .clone() + let master_config = master_config .decrypt_inner(kms_client) .await .change_context(StorageError::InitializationError) .attach_printable("Failed to decrypt master database config")?; - #[cfg(not(feature = "kms"))] - let master_config = config.master_database.clone().into(); + + #[cfg(feature = "olap")] + let replica_config = config.replica_database.clone(); + + #[cfg(all(feature = "olap", feature = "hashicorp-vault"))] + let replica_config = replica_config + .fetch_inner::(hc_client) + .await + .change_context(StorageError::InitializationError) + .attach_printable("Failed to fetch data from hashicorp vault")?; #[cfg(all(feature = "olap", feature = "kms"))] - let replica_config = config - .replica_database - .clone() + let replica_config = replica_config .decrypt_inner(kms_client) .await .change_context(StorageError::InitializationError) .attach_printable("Failed to decrypt replica database config")?; - #[cfg(all(feature = "olap", not(feature = "kms")))] - let replica_config = config.replica_database.clone().into(); - let master_enc_key = get_master_enc_key( config, #[cfg(feature = "kms")] kms_client, + #[cfg(feature = "hashicorp-vault")] + hc_client, ) .await; #[cfg(not(feature = "olap"))] - let conf = master_config; + let conf = master_config.into(); #[cfg(feature = "olap")] - let conf = (master_config, replica_config); + // this would get abstracted, for all cases + #[allow(clippy::useless_conversion)] + let conf = (master_config.into(), replica_config.into()); let store: RouterStore = if test_transaction { RouterStore::test_store(conf, &config.redis, master_enc_key).await? @@ -102,21 +129,26 @@ pub async fn get_store( async fn get_master_enc_key( conf: &crate::configs::settings::Settings, #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] + hc_client: &external_services::hashicorp_vault::HashiCorpVault, ) -> StrongSecret> { + let master_enc_key = conf.secrets.master_enc_key.clone(); + + #[cfg(feature = "hashicorp-vault")] + let master_enc_key = master_enc_key + .fetch_inner::(hc_client) + .await + .expect("Failed to fetch master enc key"); + #[cfg(feature = "kms")] - let master_enc_key = hex::decode( - conf.secrets - .master_enc_key - .clone() + let master_enc_key = masking::Secret::<_, masking::WithType>::new( + master_enc_key .decrypt_inner(kms_client) .await .expect("Failed to decrypt master enc key"), - ) - .expect("Failed to decode from hex"); + ); - #[cfg(not(feature = "kms"))] - let master_enc_key = - hex::decode(conf.secrets.master_enc_key.peek()).expect("Failed to decode from hex"); + let master_enc_key = hex::decode(master_enc_key.peek()).expect("Failed to decode from hex"); StrongSecret::new(master_enc_key) } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 0a8b84ffd11c..aec0b9bde9e9 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -12,13 +12,15 @@ use std::{ use actix_web::{body, web, FromRequest, HttpRequest, HttpResponse, Responder, ResponseError}; use api_models::enums::CaptureMethod; pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient}; +use common_enums::Currency; pub use common_utils::request::{ContentType, Method, Request, RequestBuilder}; use common_utils::{ consts::X_HS_LATENCY, errors::{ErrorSwitch, ReportSwitchExt}, + request::RequestContent, }; use error_stack::{report, IntoReport, Report, ResultExt}; -use masking::{ExposeOptionInterface, PeekInterface}; +use masking::{PeekInterface, Secret}; use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag}; use serde::Serialize; use serde_json::json; @@ -34,7 +36,10 @@ use crate::{ errors::{self, CustomResult}, payments, }, - events::api_logs::{ApiEvent, ApiEventMetric, ApiEventsType}, + events::{ + api_logs::{ApiEvent, ApiEventMetric, ApiEventsType}, + connector_api_logs::ConnectorEvent, + }, logger, routes::{ app::AppStateInfo, @@ -96,10 +101,6 @@ pub trait ConnectorValidation: ConnectorCommon { fn is_webhook_source_verification_mandatory(&self) -> bool { false } - - fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented(format!("Surcharge for {}", self.id())).into()) - } } #[async_trait::async_trait] @@ -133,8 +134,8 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny, _connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - Ok(None) + ) -> CustomResult { + Ok(RequestContent::Json(Box::new(json!(r#"{}"#)))) } fn get_request_form_data( @@ -224,6 +225,7 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny { @@ -302,6 +305,7 @@ where status_code: 200, // This status code is ignored in redirection response it will override with 302 status code. reason: None, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -349,10 +353,67 @@ where match connector_request { Some(request) => { logger::debug!(connector_request=?request); + + let masked_request_body = match &request.body { + Some(request) => match request { + RequestContent::Json(i) + | RequestContent::FormUrlEncoded(i) + | RequestContent::Xml(i) => i + .masked_serialize() + .unwrap_or(json!({ "error": "failed to mask serialize"})), + RequestContent::FormData(_) => json!({"request_type": "FORM_DATA"}), + }, + None => serde_json::Value::Null, + }; + let request_url = request.url.clone(); + let request_method = request.method; + let current_time = Instant::now(); let response = call_connector_api(state, request).await; let external_latency = current_time.elapsed().as_millis(); logger::debug!(connector_response=?response); + let status_code = response + .as_ref() + .map(|i| { + i.as_ref() + .map_or_else(|value| value.status_code, |value| value.status_code) + }) + .unwrap_or_default(); + let connector_event = ConnectorEvent::new( + req.connector.clone(), + std::any::type_name::(), + masked_request_body, + response + .as_ref() + .map(|response| { + response + .as_ref() + .map_or_else(|value| value, |value| value) + .response + .escape_ascii() + .to_string() + }) + .ok(), + request_url, + request_method, + req.payment_id.clone(), + req.merchant_id.clone(), + state.request_id.as_ref(), + external_latency, + req.refund_id.clone(), + req.dispute_id.clone(), + status_code, + ); + + match connector_event.try_into() { + Ok(event) => { + state.event_handler().log_event(event); + } + Err(err) => { + logger::error!(error=?err, "Error Logging Connector Event"); + } + } + match response { Ok(body) => { let response = match body { @@ -427,6 +488,7 @@ where reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), status_code: 504, attempt_status: None, + connector_transaction_id: None, }; router_data.response = Err(error_response); router_data.connector_http_status_code = Some(504); @@ -474,7 +536,7 @@ pub async fn send_request( request: Request, option_timeout_secs: Option, ) -> CustomResult { - logger::debug!(method=?request.method, headers=?request.headers, payload=?request.payload, ?request); + logger::debug!(method=?request.method, headers=?request.headers, payload=?request.body, ?request); let url = reqwest::Url::parse(&request.url) .into_report() @@ -505,43 +567,49 @@ pub async fn send_request( Method::Get => client.get(url), Method::Post => { let client = client.post(url); - match request.content_type { - Some(ContentType::Json) => client.json(&request.payload), - - Some(ContentType::FormData) => { - client.multipart(request.form_data.unwrap_or_default()) + match request.body { + Some(RequestContent::Json(payload)) => client.json(&payload), + Some(RequestContent::FormData(form)) => client.multipart(form), + Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload), + Some(RequestContent::Xml(payload)) => { + let body = quick_xml::se::to_string(&payload) + .into_report() + .change_context(errors::ApiClientError::BodySerializationFailed)?; + client.body(body).header("Content-Type", "application/xml") } - - // Currently this is not used remove this if not required - // If using this then handle the serde_part - Some(ContentType::FormUrlEncoded) => { - let payload = match request.payload.clone() { - Some(req) => serde_json::from_str(req.peek()) - .into_report() - .change_context(errors::ApiClientError::UrlEncodingFailed)?, - _ => json!(r#""#), - }; - let url_encoded_payload = serde_urlencoded::to_string(&payload) + None => client, + } + } + Method::Put => { + let client = client.put(url); + match request.body { + Some(RequestContent::Json(payload)) => client.json(&payload), + Some(RequestContent::FormData(form)) => client.multipart(form), + Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload), + Some(RequestContent::Xml(payload)) => { + let body = quick_xml::se::to_string(&payload) .into_report() - .change_context(errors::ApiClientError::UrlEncodingFailed) - .attach_printable_lazy(|| { - format!( - "Unable to do url encoding on request: {:?}", - &request.payload - ) - })?; - - logger::debug!(?url_encoded_payload); - client.body(url_encoded_payload) + .change_context(errors::ApiClientError::BodySerializationFailed)?; + client.body(body).header("Content-Type", "application/xml") } - // If payload needs processing the body cannot have default - None => client.body(request.payload.expose_option().unwrap_or_default()), + None => client, + } + } + Method::Patch => { + let client = client.patch(url); + match request.body { + Some(RequestContent::Json(payload)) => client.json(&payload), + Some(RequestContent::FormData(form)) => client.multipart(form), + Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload), + Some(RequestContent::Xml(payload)) => { + let body = quick_xml::se::to_string(&payload) + .into_report() + .change_context(errors::ApiClientError::BodySerializationFailed)?; + client.body(body).header("Content-Type", "application/xml") + } + None => client, } } - - Method::Put => client - .put(url) - .body(request.payload.expose_option().unwrap_or_default()), // If payload needs processing the body cannot have default Method::Delete => client.delete(url), } .add_headers(headers) @@ -674,11 +742,17 @@ pub enum ApplicationResponse { TextPlain(String), JsonForRedirection(api::RedirectionResponse), Form(Box), - PaymenkLinkForm(Box), + PaymenkLinkForm(Box), FileData((Vec, mime::Mime)), JsonWithHeaders((R, Vec<(String, String)>)), } +#[derive(Debug, Eq, PartialEq)] +pub enum PaymentLinkAction { + PaymentLinkFormData(PaymentLinkFormData), + PaymentLinkStatus(PaymentLinkStatusData), +} + #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentLinkFormData { pub js_script: String, @@ -686,6 +760,12 @@ pub struct PaymentLinkFormData { pub sdk_url: String, } +#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentLinkStatusData { + pub js_script: String, + pub css_script: String, +} + #[derive(Debug, Eq, PartialEq)] pub struct RedirectionFormData { pub redirect_form: RedirectForm, @@ -718,12 +798,28 @@ pub enum RedirectForm { BlueSnap { payment_fields_token: String, // payment-field-token }, + CybersourceAuthSetup { + access_token: String, + ddc_url: String, + reference_id: String, + }, + CybersourceConsumerAuth { + access_token: String, + step_up_url: String, + }, Payme, Braintree { client_token: String, card_token: String, bin: String, }, + Nmi { + amount: String, + currency: Currency, + public_key: Secret, + customer_vault_id: String, + order_id: String, + }, } impl From<(url::Url, Method)> for RedirectForm { @@ -769,7 +865,7 @@ where T: Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, E: ErrorSwitch + 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 +922,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 +952,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 +975,10 @@ where serialized_response, overhead_latency, auth_type, + error, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, + request.method(), ); match api_event.clone().try_into() { Ok(event) => { @@ -970,16 +1081,32 @@ where .map_into_boxed_body() } - Ok(ApplicationResponse::PaymenkLinkForm(payment_link_data)) => { - match build_payment_link_html(*payment_link_data) { - Ok(rendered_html) => http_response_html_data(rendered_html), - Err(_) => http_response_err( - r#"{ - "error": { - "message": "Error while rendering payment link html page" - } - }"#, - ), + Ok(ApplicationResponse::PaymenkLinkForm(boxed_payment_link_data)) => { + match *boxed_payment_link_data { + PaymentLinkAction::PaymentLinkFormData(payment_link_data) => { + match build_payment_link_html(payment_link_data) { + Ok(rendered_html) => http_response_html_data(rendered_html), + Err(_) => http_response_err( + r#"{ + "error": { + "message": "Error while rendering payment link html page" + } + }"#, + ), + } + } + PaymentLinkAction::PaymentLinkStatus(payment_link_data) => { + match get_payment_link_status(payment_link_data) { + Ok(rendered_html) => http_response_html_data(rendered_html), + Err(_) => http_response_err( + r#"{ + "error": { + "message": "Error while rendering payment link status page" + } + }"#, + ), + } + } } } @@ -1013,7 +1140,6 @@ where status_code = response_code, time_taken_ms = request_duration.as_millis(), ); - res } @@ -1067,6 +1193,14 @@ pub fn http_response_json(response: T) -> HttpRe .body(response) } +pub fn http_server_error_json_response( + response: T, +) -> HttpResponse { + HttpResponse::InternalServerError() + .content_type(mime::APPLICATION_JSON) + .body(response) +} + pub fn http_response_json_with_headers( response: T, mut headers: Vec<(String, String)>, @@ -1168,7 +1302,10 @@ impl Authenticate for api_models::payments::PaymentsSessionRequest { impl Authenticate for api_models::payments::PaymentsRetrieveRequest {} impl Authenticate for api_models::payments::PaymentsCancelRequest {} impl Authenticate for api_models::payments::PaymentsCaptureRequest {} +impl Authenticate for api_models::payments::PaymentsIncrementalAuthorizationRequest {} impl Authenticate for api_models::payments::PaymentsStartRequest {} +// impl Authenticate for api_models::payments::PaymentsApproveRequest {} +impl Authenticate for api_models::payments::PaymentsRejectRequest {} pub fn build_redirection_form( form: &RedirectForm, @@ -1306,6 +1443,105 @@ pub fn build_redirection_form( "))) }} } + RedirectForm::CybersourceAuthSetup { + access_token, + ddc_url, + reference_id, + } => { + maud::html! { + (maud::DOCTYPE) + html { + head { + meta name="viewport" content="width=device-width, initial-scale=1"; + } + body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { + + div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" } + + (PreEscaped(r#""#)) + + (PreEscaped(r#" + + "#)) + + + h3 style="text-align: center;" { "Please wait while we process your payment..." } + } + + (PreEscaped(r#""#)) + (PreEscaped(format!("
+ +
"))) + (PreEscaped(r#""#)) + (PreEscaped(format!(" + "))) + }} + } + RedirectForm::CybersourceConsumerAuth { + access_token, + step_up_url, + } => { + maud::html! { + (maud::DOCTYPE) + html { + head { + meta name="viewport" content="width=device-width, initial-scale=1"; + } + body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { + + div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" } + + (PreEscaped(r#""#)) + + (PreEscaped(r#" + + "#)) + + + h3 style="text-align: center;" { "Please wait while we process your payment..." } + } + + // This is the iframe recommended by cybersource but the redirection happens inside this iframe once otp + // is received and we lose control of the redirection on user client browser, so to avoid that we have removed this iframe and directly consumed it. + // (PreEscaped(r#""#)) + (PreEscaped(format!("
+ +
"))) + (PreEscaped(r#""#)) + }} + } RedirectForm::Payme => { maud::html! { (maud::DOCTYPE) @@ -1429,33 +1665,136 @@ pub fn build_redirection_form( ))) }} } - } -} + RedirectForm::Nmi { + amount, + currency, + public_key, + customer_vault_id, + order_id, + } => { + let public_key_val = public_key.peek(); + maud::html! { + (maud::DOCTYPE) + head { + (PreEscaped(r#""#)) + } + (PreEscaped(format!("" + ))) + } + } } } pub fn build_payment_link_html( payment_link_data: PaymentLinkFormData, ) -> CustomResult { - let html_template = include_str!("../core/payment_link/payment_link.html").to_string(); - let mut tera = Tera::default(); + // Add modification to css template with dynamic data + let css_template = + include_str!("../core/payment_link/payment_link_initiate/payment_link.css").to_string(); + let _ = tera.add_raw_template("payment_link_css", &css_template); + let mut context = Context::new(); + context.insert("css_color_scheme", &payment_link_data.css_script); + + let rendered_css = match tera.render("payment_link_css", &context) { + Ok(rendered_css) => rendered_css, + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + }; + + // Add modification to js template with dynamic data + let js_template = + include_str!("../core/payment_link/payment_link_initiate/payment_link.js").to_string(); + let _ = tera.add_raw_template("payment_link_js", &js_template); + + context.insert("payment_details_js_script", &payment_link_data.js_script); + + let rendered_js = match tera.render("payment_link_js", &context) { + Ok(rendered_js) => rendered_js, + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + }; + + // Modify Html template with rendered js and rendered css files + let html_template = + include_str!("../core/payment_link/payment_link_initiate/payment_link.html").to_string(); + let _ = tera.add_raw_template("payment_link", &html_template); - let mut context = Context::new(); context.insert( "hyperloader_sdk_link", &get_hyper_loader_sdk(&payment_link_data.sdk_url), ); - context.insert("css_color_scheme", &payment_link_data.css_script); - context.insert("payment_details_js_script", &payment_link_data.js_script); + context.insert("rendered_css", &rendered_css); + context.insert("rendered_js", &rendered_js); match tera.render("payment_link", &context) { Ok(rendered_html) => Ok(rendered_html), @@ -1467,5 +1806,65 @@ pub fn build_payment_link_html( } fn get_hyper_loader_sdk(sdk_url: &str) -> String { - format!("") + format!("") +} + +pub fn get_payment_link_status( + payment_link_data: PaymentLinkStatusData, +) -> CustomResult { + let mut tera = Tera::default(); + + // Add modification to css template with dynamic data + let css_template = + include_str!("../core/payment_link/payment_link_status/status.css").to_string(); + let _ = tera.add_raw_template("payment_link_css", &css_template); + let mut context = Context::new(); + context.insert("css_color_scheme", &payment_link_data.css_script); + + let rendered_css = match tera.render("payment_link_css", &context) { + Ok(rendered_css) => rendered_css, + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + }; + + // Add modification to js template with dynamic data + let js_template = + include_str!("../core/payment_link/payment_link_status/status.js").to_string(); + let _ = tera.add_raw_template("payment_link_js", &js_template); + context.insert("payment_details_js_script", &payment_link_data.js_script); + + let rendered_js = match tera.render("payment_link_js", &context) { + Ok(rendered_js) => rendered_js, + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + }; + + // Modify Html template with rendered js and rendered css files + let html_template = + include_str!("../core/payment_link/payment_link_status/status.html").to_string(); + let _ = tera.add_raw_template("payment_link_status", &html_template); + + context.insert("rendered_css", &rendered_css); + + context.insert("rendered_js", &rendered_js); + + match tera.render("payment_link_status", &context) { + Ok(rendered_html) => Ok(rendered_html), + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_mime_essence() { + assert_eq!(mime::APPLICATION_JSON.essence_str(), "application/json"); + } } diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index 8eb6ab72f988..f4d74c4f81bb 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -10,6 +10,7 @@ use router_env::tracing_actix_web::RequestId; use super::{request::Maskable, Request}; use crate::{ configs::settings::{Locker, Proxy}, + consts::LOCKER_HEALTH_CALL_PATH, core::{ errors::{ApiClientError, CustomResult}, payments, @@ -110,18 +111,18 @@ pub(super) fn create_client( pub fn proxy_bypass_urls(locker: &Locker) -> Vec { let locker_host = locker.host.to_owned(); - let basilisk_host = locker.basilisk_host.to_owned(); + let locker_host_rs = locker.host_rs.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_rs}{}", LOCKER_HEALTH_CALL_PATH), format!("{locker_host}/card/addCard"), format!("{locker_host}/card/getCard"), format!("{locker_host}/card/deleteCard"), - format!("{basilisk_host}/tokenize"), - format!("{basilisk_host}/tokenize/get"), - format!("{basilisk_host}/tokenize/delete"), - format!("{basilisk_host}/tokenize/delete/token"), ] } diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index da4dec2eec8a..221106612f35 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -3,16 +3,27 @@ use api_models::{payment_methods::PaymentMethodListRequest, payments}; use async_trait::async_trait; use common_utils::date_time; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +#[cfg(feature = "hashicorp-vault")] +use masking::ExposeInterface; use masking::{PeekInterface, StrongSecret}; use serde::Serialize; +use super::authorization::{self, permissions::Permission}; #[cfg(feature = "olap")] use super::jwt; +#[cfg(feature = "recon")] +use super::recon::ReconToken; #[cfg(feature = "olap")] use crate::consts; +#[cfg(feature = "olap")] +use crate::core::errors::UserResult; +#[cfg(feature = "recon")] +use crate::routes::AppState; use crate::{ configs::settings, core::{ @@ -25,6 +36,7 @@ use crate::{ types::domain, utils::OptionExt, }; +pub mod blacklist; #[derive(Clone, Debug)] pub struct AuthenticationData { @@ -44,16 +56,22 @@ pub enum AuthenticationType { key_id: String, }, AdminApiKey, - MerchantJWT { + MerchantJwt { merchant_id: String, user_id: Option, }, - MerchantID { + UserJwt { + user_id: String, + }, + MerchantId { merchant_id: String, }, PublishableKey { merchant_id: String, }, + WebhookAuth { + merchant_id: String, + }, NoAuth, } @@ -64,17 +82,39 @@ 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::AdminApiKey | Self::NoAuth => None, + } + | Self::WebhookAuth { merchant_id } => Some(merchant_id.as_ref()), + Self::AdminApiKey | Self::UserJwt { .. } | Self::NoAuth => None, } } } +#[derive(Clone, Debug)] +pub struct UserWithoutMerchantFromToken { + pub user_id: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct UserAuthToken { + pub user_id: String, + pub exp: u64, +} + +#[cfg(feature = "olap")] +impl UserAuthToken { + pub async fn new_token(user_id: String, settings: &settings::Settings) -> 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 { user_id, exp }; + jwt::generate_jwt(&token_payload, settings).await + } +} + #[derive(serde::Serialize, serde::Deserialize)] pub struct AuthToken { pub user_id: String, @@ -92,7 +132,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 +146,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>; } @@ -179,6 +227,10 @@ where &config.api_keys, #[cfg(feature = "kms")] kms::get_kms_client(&config.kms).await, + #[cfg(feature = "hashicorp-vault")] + external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?, ) .await? }; @@ -238,9 +290,14 @@ static ADMIN_API_KEY: tokio::sync::OnceCell> = pub async fn get_admin_api_key( secrets: &settings::Secrets, #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] + hc_client: &external_services::hashicorp_vault::HashiCorpVault, ) -> RouterResult<&'static StrongSecret> { ADMIN_API_KEY .get_or_try_init(|| async { + #[cfg(not(feature = "kms"))] + let admin_api_key = secrets.admin_api_key.clone(); + #[cfg(feature = "kms")] let admin_api_key = secrets .kms_encrypted_admin_api_key @@ -249,14 +306,49 @@ pub async fn get_admin_api_key( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to KMS decrypt admin API key")?; - #[cfg(not(feature = "kms"))] - let admin_api_key = secrets.admin_api_key.clone(); + #[cfg(feature = "hashicorp-vault")] + let admin_api_key = masking::Secret::new(admin_api_key) + .fetch_inner::(hc_client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to KMS decrypt admin API key")? + .expose(); Ok(StrongSecret::new(admin_api_key)) }) .await } +#[derive(Debug)] +pub struct UserWithoutMerchantJWTAuth; + +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch for UserWithoutMerchantJWTAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(UserWithoutMerchantFromToken, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + Ok(( + UserWithoutMerchantFromToken { + user_id: payload.user_id.clone(), + }, + AuthenticationType::UserJwt { + user_id: payload.user_id, + }, + )) + } +} + #[derive(Debug)] pub struct AdminApiAuth; @@ -278,6 +370,11 @@ where &conf.secrets, #[cfg(feature = "kms")] kms::get_kms_client(&conf.kms).await, + #[cfg(feature = "hashicorp-vault")] + external_services::hashicorp_vault::get_hashicorp_client(&conf.hc_vault) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting admin api key")?, ) .await?; @@ -337,7 +434,7 @@ where }; Ok(( auth.clone(), - AuthenticationType::MerchantID { + AuthenticationType::MerchantId { merchant_id: auth.merchant_account.merchant_id.clone(), }, )) @@ -383,7 +480,7 @@ where } #[derive(Debug)] -pub(crate) struct JWTAuth; +pub(crate) struct JWTAuth(pub Permission); #[derive(serde::Deserialize)] struct JwtAuthPayloadFetchUnit { @@ -402,9 +499,50 @@ where state: &A, ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + 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?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + 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 +552,7 @@ where pub struct JWTAuthMerchantFromRoute { pub merchant_id: String, + pub required_permission: Permission, } #[async_trait] @@ -427,14 +566,20 @@ where state: &A, ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } - // 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), }, @@ -453,11 +598,6 @@ where Ok(payload) } -#[derive(serde::Deserialize)] -struct JwtAuthPayloadFetchMerchantAccount { - merchant_id: String, -} - #[async_trait] impl AuthenticateAndFetch for JWTAuth where @@ -468,9 +608,14 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { - let payload = - parse_jwt_payload::(request_headers, state) - .await?; + let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + 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 +638,7 @@ where }; Ok(( auth.clone(), - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: auth.merchant_account.merchant_id.clone(), user_id: None, }, @@ -501,6 +646,109 @@ where } } +pub type AuthenticationDataWithUserId = (AuthenticationData, String); + +#[async_trait] +impl AuthenticateAndFetch for JWTAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationDataWithUserId, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + 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( + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let merchant = state + .store() + .find_merchant_account_by_merchant_id(&payload.merchant_id, &key_store) + .await + .change_context(errors::ApiErrorResponse::InvalidJwtToken)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + }; + Ok(( + (auth.clone(), payload.user_id.clone()), + AuthenticationType::MerchantJwt { + merchant_id: auth.merchant_account.merchant_id.clone(), + user_id: None, + }, + )) + } +} + +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?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + 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)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + Ok(((), AuthenticationType::NoAuth)) + } +} + pub trait ClientSecretFetch { fn get_client_secret(&self) -> Option<&String>; } @@ -535,6 +783,18 @@ impl ClientSecretFetch for api_models::payments::RetrievePaymentLinkRequest { } } +impl ClientSecretFetch for api_models::pm_auth::LinkTokenCreateRequest { + fn get_client_secret(&self) -> Option<&String> { + self.client_secret.as_ref() + } +} + +impl ClientSecretFetch for api_models::pm_auth::ExchangeTokenCreateRequest { + fn get_client_secret(&self) -> Option<&String> { + self.client_secret.as_ref() + } +} + pub fn get_auth_type_and_flow( headers: &HeaderMap, ) -> RouterResult<( @@ -704,3 +964,95 @@ where } default_auth } + +#[cfg(feature = "recon")] +static RECON_API_KEY: tokio::sync::OnceCell> = + tokio::sync::OnceCell::const_new(); + +#[cfg(feature = "recon")] +pub async fn get_recon_admin_api_key( + secrets: &settings::Secrets, + #[cfg(feature = "kms")] kms_client: &kms::KmsClient, +) -> RouterResult<&'static StrongSecret> { + RECON_API_KEY + .get_or_try_init(|| async { + #[cfg(feature = "kms")] + let recon_admin_api_key = secrets + .kms_encrypted_recon_admin_api_key + .decrypt_inner(kms_client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to KMS decrypt recon admin API key")?; + + #[cfg(not(feature = "kms"))] + let recon_admin_api_key = secrets.recon_admin_api_key.clone(); + + Ok(StrongSecret::new(recon_admin_api_key)) + }) + .await +} + +#[cfg(feature = "recon")] +pub struct ReconAdmin; + +#[async_trait] +#[cfg(feature = "recon")] +impl AuthenticateAndFetch<(), A> for ReconAdmin +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<((), AuthenticationType)> { + let request_admin_api_key = + get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?; + let conf = state.conf(); + + let admin_api_key = get_recon_admin_api_key( + &conf.secrets, + #[cfg(feature = "kms")] + kms::get_kms_client(&conf.kms).await, + ) + .await?; + + if request_admin_api_key != admin_api_key.peek() { + Err(report!(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Recon Admin Authentication Failure"))?; + } + + Ok(((), AuthenticationType::NoAuth)) + } +} + +#[cfg(feature = "recon")] +pub struct ReconJWT; +#[cfg(feature = "recon")] +pub struct ReconUser { + pub user_id: String, +} +#[cfg(feature = "recon")] +impl AuthInfo for ReconUser { + fn get_merchant_id(&self) -> Option<&str> { + None + } +} +#[cfg(all(feature = "olap", feature = "recon"))] +#[async_trait] +impl AuthenticateAndFetch for ReconJWT { + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &AppState, + ) -> RouterResult<(ReconUser, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + Ok(( + ReconUser { + user_id: payload.user_id, + }, + AuthenticationType::NoAuth, + )) + } +} diff --git a/crates/router/src/services/authentication/blacklist.rs b/crates/router/src/services/authentication/blacklist.rs new file mode 100644 index 000000000000..6fab28433b48 --- /dev/null +++ b/crates/router/src/services/authentication/blacklist.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +#[cfg(feature = "olap")] +use common_utils::date_time; +use error_stack::{IntoReport, ResultExt}; +use redis_interface::RedisConnectionPool; + +use crate::{ + consts::{JWT_TOKEN_TIME_IN_SECS, USER_BLACKLIST_PREFIX}, + core::errors::{ApiErrorResponse, RouterResult}, + routes::app::AppStateInfo, +}; +#[cfg(feature = "olap")] +use crate::{ + core::errors::{UserErrors, UserResult}, + routes::AppState, +}; + +#[cfg(feature = "olap")] +pub async fn insert_user_in_blacklist(state: &AppState, user_id: &str) -> UserResult<()> { + let user_blacklist_key = format!("{}{}", USER_BLACKLIST_PREFIX, user_id); + let expiry = + expiry_to_i64(JWT_TOKEN_TIME_IN_SECS).change_context(UserErrors::InternalServerError)?; + let redis_conn = get_redis_connection(state).change_context(UserErrors::InternalServerError)?; + redis_conn + .set_key_with_expiry( + user_blacklist_key.as_str(), + date_time::now_unix_timestamp(), + expiry, + ) + .await + .change_context(UserErrors::InternalServerError) +} + +pub async fn check_user_in_blacklist( + state: &A, + user_id: &str, + token_expiry: u64, +) -> RouterResult { + let token = format!("{}{}", USER_BLACKLIST_PREFIX, user_id); + let token_issued_at = expiry_to_i64(token_expiry - JWT_TOKEN_TIME_IN_SECS)?; + let redis_conn = get_redis_connection(state)?; + redis_conn + .get_key::>(token.as_str()) + .await + .change_context(ApiErrorResponse::InternalServerError) + .map(|timestamp| timestamp.map_or(false, |timestamp| timestamp > token_issued_at)) +} + +fn get_redis_connection(state: &A) -> RouterResult> { + state + .store() + .get_redis_conn() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection") +} + +fn expiry_to_i64(expiry: u64) -> RouterResult { + expiry + .try_into() + .into_report() + .change_context(ApiErrorResponse::InternalServerError) +} 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..99e4f1b6c096 --- /dev/null +++ b/crates/router/src/services/authorization/info.rs @@ -0,0 +1,182 @@ +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 { + permissions + .iter() + .map(|&per| Self { + description: Permission::get_permission_description(&per), + enum_name: per, + }) + .collect() + } +} + +#[derive(PartialEq, EnumIter, Clone)] +pub enum PermissionModule { + Payments, + Refunds, + MerchantAccount, + Connectors, + Forex, + Routing, + Analytics, + Mandates, + Customer, + Disputes, + Files, + ThreeDsDecisionManager, + SurchargeDecisionManager, + AccountCreate, +} + +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::Customer => "Everything related to customers - like creating and viewing customer 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", + Self::AccountCreate => "Create new account within your organization" + } + } +} + +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::Customer => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::CustomerRead, + Permission::CustomerWrite, + ]), + }, + 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, + ]), + }, + PermissionModule::AccountCreate => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::MerchantAccountCreate]), + }, + } + } +} diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs new file mode 100644 index 000000000000..5c5e3ecce300 --- /dev/null +++ b/crates/router/src/services/authorization/permissions.rs @@ -0,0 +1,76 @@ +use strum::Display; + +#[derive(PartialEq, Display, Clone, Debug, Copy)] +pub enum Permission { + PaymentRead, + PaymentWrite, + RefundRead, + RefundWrite, + ApiKeyRead, + ApiKeyWrite, + MerchantAccountRead, + MerchantAccountWrite, + MerchantConnectorAccountRead, + MerchantConnectorAccountWrite, + ForexRead, + RoutingRead, + RoutingWrite, + DisputeRead, + DisputeWrite, + MandateRead, + MandateWrite, + CustomerRead, + CustomerWrite, + FileRead, + FileWrite, + Analytics, + ThreeDsDecisionManagerWrite, + ThreeDsDecisionManagerRead, + SurchargeDecisionManagerWrite, + SurchargeDecisionManagerRead, + UsersRead, + UsersWrite, + MerchantAccountCreate, +} + +impl Permission { + pub fn get_permission_description(&self) -> &'static str { + match self { + Self::PaymentRead => "View all payments", + Self::PaymentWrite => "Create payment, download payments data", + Self::RefundRead => "View all refunds", + Self::RefundWrite => "Create refund, download refunds data", + Self::ApiKeyRead => "View API keys (masked generated for the system", + Self::ApiKeyWrite => "Create and update API keys", + Self::MerchantAccountRead => "View merchant account details", + Self::MerchantAccountWrite => { + "Update merchant account details, configure webhooks, manage api keys" + } + Self::MerchantConnectorAccountRead => "View connectors configured", + Self::MerchantConnectorAccountWrite => { + "Create, update, verify and delete connector configurations" + } + Self::ForexRead => "Query Forex data", + Self::RoutingRead => "View routing configuration", + Self::RoutingWrite => "Create and activate routing configurations", + Self::DisputeRead => "View disputes", + Self::DisputeWrite => "Create and update disputes", + Self::MandateRead => "View mandates", + Self::MandateWrite => "Create and update mandates", + Self::CustomerRead => "View customers", + Self::CustomerWrite => "Create, update and delete customers", + Self::FileRead => "View files", + Self::FileWrite => "Create, update and delete files", + Self::Analytics => "Access to analytics module", + Self::ThreeDsDecisionManagerWrite => "Create and update 3DS decision rules", + Self::ThreeDsDecisionManagerRead => { + "View all 3DS decision rules configured for a merchant" + } + Self::SurchargeDecisionManagerWrite => "Create and update the surcharge decision rules", + Self::SurchargeDecisionManagerRead => "View all the surcharge decision rules", + Self::UsersRead => "View all the users for a merchant", + Self::UsersWrite => "Invite users, assign and update roles", + Self::MerchantAccountCreate => "Create merchant account", + } + } +} 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..6fe0ddcc3605 --- /dev/null +++ b/crates/router/src/services/authorization/predefined_permissions.rs @@ -0,0 +1,325 @@ +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, + is_deletable: 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::CustomerRead, + Permission::CustomerWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + Permission::MerchantAccountCreate, + ], + name: None, + is_invitable: false, + is_deletable: 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::CustomerRead, + Permission::FileRead, + Permission::UsersRead, + ], + name: None, + is_invitable: false, + is_deletable: 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::CustomerRead, + Permission::CustomerWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + Permission::MerchantAccountCreate, + ], + name: Some("Organization Admin"), + is_invitable: false, + is_deletable: 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::CustomerRead, + Permission::CustomerWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + ], + name: Some("Admin"), + is_invitable: true, + is_deletable: 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::CustomerRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("View Only"), + is_invitable: true, + is_deletable: 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::CustomerRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + ], + name: Some("IAM"), + is_invitable: true, + is_deletable: 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::CustomerRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("Developer"), + is_invitable: true, + is_deletable: 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::CustomerRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("Operator"), + is_invitable: true, + is_deletable: 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::CustomerRead, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + ], + name: Some("Customer Support"), + is_invitable: true, + is_deletable: 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) +} + +pub fn is_role_deletable(role_id: &str) -> bool { + PREDEFINED_PERMISSIONS + .get(role_id) + .map_or(false, |role_info| role_info.is_deletable) +} 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/bizemailprod.html b/crates/router/src/services/email/assets/bizemailprod.html new file mode 100644 index 000000000000..c705608ec720 --- /dev/null +++ b/crates/router/src/services/email/assets/bizemailprod.html @@ -0,0 +1,138 @@ + +Welcome to HyperSwitch! + +
+ + + + + + + + + + + + + + + + + + + + +
+
+

Hi Team,

+

+ A Production Account Intent has been initiated by {username} - + please find more details below: +

+
    +
  1. Name: {username}
  2. +
  3. Point of Contact Email (POC): {poc_email}
  4. +
  5. Legal Business Name: {legal_business_name}
  6. +
  7. Business Location: {business_location}
  8. +
  9. Business Website: {business_website}
  10. +
+
+
+ Regards,
+ Hyperswitch Dashboard Team +
+
+ 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..643b6e230633 --- /dev/null +++ b/crates/router/src/services/email/assets/magic_link.html @@ -0,0 +1,256 @@ + +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_activation.html b/crates/router/src/services/email/assets/recon_activation.html new file mode 100644 index 000000000000..7feffacb09df --- /dev/null +++ b/crates/router/src/services/email/assets/recon_activation.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..6ad1a0eb99ad --- /dev/null +++ b/crates/router/src/services/email/types.rs @@ -0,0 +1,383 @@ +use api_models::user::dashboard_metadata::ProdIntent; +use common_utils::errors::CustomResult; +use error_stack::ResultExt; +use external_services::email::{EmailContents, EmailData, EmailError}; +use masking::{ExposeInterface, PeekInterface, Secret}; + +use crate::{configs, consts, routes::AppState}; +#[cfg(feature = "olap")] +use crate::{ + core::errors::{UserErrors, UserResult}, + services::jwt, + types::domain, +}; + +pub enum EmailBody { + Verify { + link: String, + }, + Reset { + link: String, + user_name: String, + }, + MagicLink { + link: String, + user_name: String, + }, + InviteUser { + link: String, + user_name: String, + }, + BizEmailProd { + user_name: String, + poc_email: String, + legal_business_name: String, + business_location: String, + business_website: String, + }, + ReconActivation { + user_name: String, + }, + ProFeatureRequest { + feature_name: String, + merchant_id: String, + user_name: String, + user_email: 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) + } + EmailBody::Reset { link, user_name } => { + format!( + include_str!("assets/reset.html"), + link = link, + username = user_name + ) + } + EmailBody::MagicLink { link, user_name } => { + format!( + include_str!("assets/magic_link.html"), + user_name = user_name, + link = link + ) + } + EmailBody::InviteUser { link, user_name } => { + format!( + include_str!("assets/invite.html"), + username = user_name, + link = link + ) + } + EmailBody::ReconActivation { user_name } => { + format!( + include_str!("assets/recon_activation.html"), + username = user_name, + ) + } + EmailBody::BizEmailProd { + user_name, + poc_email, + legal_business_name, + business_location, + business_website, + } => { + format!( + include_str!("assets/bizemailprod.html"), + poc_email = poc_email, + legal_business_name = legal_business_name, + business_location = business_location, + business_website = business_website, + username = user_name, + ) + } + EmailBody::ProFeatureRequest { + feature_name, + merchant_id, + user_name, + user_email, + } => format!( + "Dear Hyperswitch Support Team, + +Dashboard Pro Feature Request, +Feature name : {feature_name} +Merchant ID : {merchant_id} +Merchant Name : {user_name} +Email : {user_email} + +(note: This is an auto generated email. Use merchant email for any further communications)", + ), + } + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct EmailToken { + email: String, + merchant_id: Option, + exp: u64, +} + +impl EmailToken { + pub async fn new_token( + email: domain::UserEmail, + merchant_id: Option, + settings: &configs::settings::Settings, + ) -> CustomResult { + let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); + let exp = jwt::generate_exp(expiration_duration)?.as_secs(); + let token_payload = Self { + email: email.get_secret().expose(), + merchant_id, + exp, + }; + jwt::generate_jwt(&token_payload, settings).await + } + + pub fn get_email(&self) -> &str { + self.email.as_str() + } + + pub fn get_merchant_id(&self) -> Option<&str> { + self.merchant_id.as_deref() + } +} + +pub fn get_link_with_token( + base_url: impl std::fmt::Display, + token: impl std::fmt::Display, + action: impl std::fmt::Display, +) -> String { + format!("{base_url}/user/{action}?token={token}") +} + +pub struct VerifyEmail { + pub recipient_email: domain::UserEmail, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +/// Currently only HTML is supported +#[async_trait::async_trait] +impl EmailData for VerifyEmail { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let verify_email_link = + get_link_with_token(&self.settings.email.base_url, token, "verify_email"); + + let body = html::get_html_body(EmailBody::Verify { + link: verify_email_link, + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct ResetPassword { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for ResetPassword { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let reset_password_link = + get_link_with_token(&self.settings.email.base_url, token, "set_password"); + + let body = html::get_html_body(EmailBody::Reset { + link: reset_password_link, + user_name: self.user_name.clone().get_secret().expose(), + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct MagicLink { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for MagicLink { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let magic_link_login = + get_link_with_token(&self.settings.email.base_url, token, "verify_email"); + + let body = html::get_html_body(EmailBody::MagicLink { + link: magic_link_login, + user_name: self.user_name.clone().get_secret().expose(), + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct InviteUser { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, + pub merchant_id: String, +} + +#[async_trait::async_trait] +impl EmailData for InviteUser { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token( + self.recipient_email.clone(), + Some(self.merchant_id.clone()), + &self.settings, + ) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let invite_user_link = + get_link_with_token(&self.settings.email.base_url, token, "set_password"); + + let body = html::get_html_body(EmailBody::MagicLink { + link: invite_user_link, + user_name: self.user_name.clone().get_secret().expose(), + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct ReconActivation { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for ReconActivation { + async fn get_email_data(&self) -> CustomResult { + let body = html::get_html_body(EmailBody::ReconActivation { + user_name: self.user_name.clone().get_secret().expose(), + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct BizEmailProd { + pub recipient_email: domain::UserEmail, + pub user_name: Secret, + pub poc_email: Secret, + pub legal_business_name: String, + pub business_location: String, + pub business_website: String, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +impl BizEmailProd { + pub fn new(state: &AppState, data: ProdIntent) -> UserResult { + Ok(Self { + recipient_email: (domain::UserEmail::new( + consts::user::BUSINESS_EMAIL.to_string().into(), + ))?, + settings: state.conf.clone(), + subject: "New Prod Intent", + user_name: data.poc_name.unwrap_or_default().into(), + poc_email: data.poc_email.unwrap_or_default().into(), + legal_business_name: data.legal_business_name.unwrap_or_default(), + business_location: data + .business_location + .unwrap_or(common_enums::CountryAlpha2::AD) + .to_string(), + business_website: data.business_website.unwrap_or_default(), + }) + } +} + +#[async_trait::async_trait] +impl EmailData for BizEmailProd { + async fn get_email_data(&self) -> CustomResult { + let body = html::get_html_body(EmailBody::BizEmailProd { + user_name: self.user_name.clone().expose(), + poc_email: self.poc_email.clone().expose(), + legal_business_name: self.legal_business_name.clone(), + business_location: self.business_location.clone(), + business_website: self.business_website.clone(), + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct ProFeatureRequest { + pub recipient_email: domain::UserEmail, + pub feature_name: String, + pub merchant_id: String, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: String, +} + +#[async_trait::async_trait] +impl EmailData for ProFeatureRequest { + async fn get_email_data(&self) -> CustomResult { + let recipient = self.recipient_email.clone().into_inner(); + + let body = html::get_html_body(EmailBody::ProFeatureRequest { + user_name: self.user_name.clone().get_secret().expose(), + feature_name: self.feature_name.clone(), + merchant_id: self.merchant_id.clone(), + user_email: recipient.peek().to_string(), + }); + + Ok(EmailContents { + subject: self.subject.clone(), + body: external_services::email::IntermediateString::new(body), + recipient, + }) + } +} diff --git a/crates/router/src/services/encryption.rs b/crates/router/src/services/encryption.rs index acea96f86607..9a8a5f4af4a9 100644 --- a/crates/router/src/services/encryption.rs +++ b/crates/router/src/services/encryption.rs @@ -1,9 +1,7 @@ -use std::{num::Wrapping, str}; +use std::str; use error_stack::{report, IntoReport, ResultExt}; use josekit::{jwe, jws}; -use rand; -use ring::{aead::*, error::Unspecified}; use serde::{Deserialize, Serialize}; use crate::{ @@ -11,44 +9,6 @@ use crate::{ utils, }; -struct NonceGen { - current: Wrapping, - start: u128, -} - -impl NonceGen { - fn new(start: [u8; 12]) -> Self { - let mut array = [0; 16]; - array[..12].copy_from_slice(&start); - let start = if cfg!(target_endian = "little") { - u128::from_le_bytes(array) - } else { - u128::from_be_bytes(array) - }; - Self { - current: Wrapping(start), - start, - } - } -} - -impl NonceSequence for NonceGen { - fn advance(&mut self) -> Result { - let n = self.current.0; - self.current += 1; - if self.current.0 == self.start { - return Err(Unspecified); - } - let value = if cfg!(target_endian = "little") { - n.to_le_bytes()[..12].try_into()? - } else { - n.to_be_bytes()[..12].try_into()? - }; - let nonce = Nonce::assume_unique_for_key(value); - Ok(nonce) - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JwsBody { pub header: String, @@ -66,57 +26,6 @@ pub struct JweBody { pub encrypted_key: String, } -pub fn encrypt(msg: &String, key: &[u8]) -> CustomResult, errors::EncryptionError> { - let nonce_seed = rand::random(); - let mut sealing_key = { - let key = UnboundKey::new(&AES_256_GCM, key) - .map_err(errors::EncryptionError::from) - .into_report() - .attach_printable("Unbound Key Error")?; - let nonce_gen = NonceGen::new(nonce_seed); - SealingKey::new(key, nonce_gen) - }; - let msg_byte = msg.as_bytes(); - let mut data = msg_byte.to_vec(); - - sealing_key - .seal_in_place_append_tag(Aad::empty(), &mut data) - .map_err(errors::EncryptionError::from) - .into_report() - .attach_printable("Error Encrypting")?; - let nonce_vec = nonce_seed.to_vec(); - data.splice(0..0, nonce_vec); //prepend nonce at the start - Ok(data) -} - -pub fn decrypt(mut data: Vec, key: &[u8]) -> CustomResult { - let nonce_seed = data[0..12] - .try_into() - .into_report() - .change_context(errors::EncryptionError) - .attach_printable("Error getting nonce")?; - data.drain(0..12); - - let mut opening_key = { - let key = UnboundKey::new(&AES_256_GCM, key) - .map_err(errors::EncryptionError::from) - .into_report() - .attach_printable("Unbound Key Error")?; - let nonce_gen = NonceGen::new(nonce_seed); - OpeningKey::new(key, nonce_gen) - }; - let res_byte = opening_key - .open_in_place(Aad::empty(), &mut data) - .map_err(errors::EncryptionError::from) - .into_report() - .attach_printable("Error Decrypting")?; - let response = str::from_utf8_mut(res_byte) - .into_report() - .change_context(errors::EncryptionError) - .attach_printable("Error from_utf8")?; - Ok(response.to_string()) -} - pub async fn encrypt_jwe( payload: &[u8], public_key: impl AsRef<[u8]>, @@ -220,7 +129,6 @@ mod tests { #![allow(clippy::unwrap_used, clippy::expect_used)] use super::*; - use crate::utils::{self, ValueExt}; // Keys used for tests // Can be generated using the following commands: @@ -308,21 +216,6 @@ VuY3OeNxi+dC2r7HppP3O/MJ4gX/RJJfSrcaGP8/Ke1W5+jE97Qy -----END RSA PRIVATE KEY----- "; - fn generate_key() -> [u8; 32] { - let key: [u8; 32] = rand::random(); - key - } - - #[test] - fn test_enc() { - let key = generate_key(); - let enc_data = encrypt(&"Test_Encrypt".to_string(), &key).unwrap(); - let card_info = utils::Encode::>::encode_to_value(&enc_data).unwrap(); - let data: Vec = card_info.parse_value("ParseEncryptedData").unwrap(); - let dec_data = decrypt(data, &key).unwrap(); - assert_eq!(dec_data, "Test_Encrypt".to_string()); - } - #[actix_rt::test] async fn test_jwe() { let jwt = encrypt_jwe("request_payload".as_bytes(), ENCRYPTION_KEY) diff --git a/crates/router/src/services/kafka.rs b/crates/router/src/services/kafka.rs new file mode 100644 index 000000000000..2bcfcfe974f1 --- /dev/null +++ b/crates/router/src/services/kafka.rs @@ -0,0 +1,331 @@ +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 payment_attempt; +mod payment_intent; +mod refund; +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, + connector_logs_topic: String, + outgoing_webhook_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(), + )) + })?; + + common_utils::fp_utils::when(self.connector_logs_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Connector Logs topic must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when( + self.outgoing_webhook_logs_topic.is_default_or_empty(), + || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Outgoing Webhook Logs topic must not be empty".into(), + )) + }, + )?; + + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct KafkaProducer { + producer: Arc, + intent_analytics_topic: String, + attempt_analytics_topic: String, + refund_analytics_topic: String, + api_logs_topic: String, + connector_logs_topic: String, + outgoing_webhook_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(), + connector_logs_topic: conf.connector_logs_topic.clone(), + outgoing_webhook_logs_topic: conf.outgoing_webhook_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 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, + EventType::ConnectorApiLogs => &self.connector_logs_topic, + EventType::OutgoingWebhookLogs => &self.outgoing_webhook_logs_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/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/services/pm_auth.rs b/crates/router/src/services/pm_auth.rs new file mode 100644 index 000000000000..7487b12663b1 --- /dev/null +++ b/crates/router/src/services/pm_auth.rs @@ -0,0 +1,95 @@ +use pm_auth::{ + consts, + core::errors::ConnectorError, + types::{self as pm_auth_types, api::BoxedConnectorIntegration, PaymentAuthRouterData}, +}; + +use crate::{ + core::errors::{self}, + logger, + routes::AppState, + services::{self}, +}; + +pub async fn execute_connector_processing_step< + 'b, + 'a, + T: 'static, + Req: Clone + 'static, + Resp: Clone + 'static, +>( + state: &'b AppState, + connector_integration: BoxedConnectorIntegration<'a, T, Req, Resp>, + req: &'b PaymentAuthRouterData, + connector: &pm_auth_types::PaymentMethodAuthConnectors, +) -> errors::CustomResult, ConnectorError> +where + T: Clone, + Req: Clone, + Resp: Clone, +{ + let mut router_data = req.clone(); + + let connector_request = connector_integration.build_request(req, connector)?; + + match connector_request { + Some(request) => { + logger::debug!(connector_request=?request); + let response = services::api::call_connector_api(state, request).await; + logger::debug!(connector_response=?response); + match response { + Ok(body) => { + let response = match body { + Ok(body) => { + let body = pm_auth_types::Response { + headers: body.headers, + response: body.response, + status_code: body.status_code, + }; + let connector_http_status_code = Some(body.status_code); + let mut data = + connector_integration.handle_response(&router_data, body)?; + data.connector_http_status_code = connector_http_status_code; + + data + } + Err(body) => { + let body = pm_auth_types::Response { + headers: body.headers, + response: body.response, + status_code: body.status_code, + }; + router_data.connector_http_status_code = Some(body.status_code); + + let error = match body.status_code { + 500..=511 => connector_integration.get_5xx_error_response(body)?, + _ => connector_integration.get_error_response(body)?, + }; + + router_data.response = Err(error); + + router_data + } + }; + Ok(response) + } + Err(error) => { + if error.current_context().is_upstream_timeout() { + let error_response = pm_auth_types::ErrorResponse { + code: consts::REQUEST_TIMEOUT_ERROR_CODE.to_string(), + message: consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string(), + reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), + status_code: 504, + }; + router_data.response = Err(error_response); + router_data.connector_http_status_code = Some(504); + Ok(router_data) + } else { + Err(error.change_context(ConnectorError::ProcessingStepFailed(None))) + } + } + } + } + None => Ok(router_data), + } +} diff --git a/crates/router/src/services/recon.rs b/crates/router/src/services/recon.rs new file mode 100644 index 000000000000..d5a2151a487b --- /dev/null +++ b/crates/router/src/services/recon.rs @@ -0,0 +1,29 @@ +use error_stack::ResultExt; +use masking::Secret; + +use super::jwt; +use crate::{ + consts, + core::{self, errors::RouterResult}, + routes::app::settings::Settings, +}; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ReconToken { + pub user_id: String, + pub exp: u64, +} + +impl ReconToken { + pub async fn new_token(user_id: String, settings: &Settings) -> RouterResult> { + let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); + let exp = jwt::generate_exp(exp_duration) + .change_context(core::errors::ApiErrorResponse::InternalServerError)? + .as_secs(); + let token_payload = Self { user_id, exp }; + let token = jwt::generate_jwt(&token_payload, settings) + .await + .change_context(core::errors::ApiErrorResponse::InternalServerError)?; + Ok(Secret::new(token)) + } +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 7cf8f6b71fa5..10e13a6af579 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -8,6 +8,10 @@ pub mod api; pub mod domain; +#[cfg(feature = "frm")] +pub mod fraud_check; +pub mod pm_auth; + pub mod storage; pub mod transformers; @@ -15,13 +19,15 @@ use std::{collections::HashMap, marker::PhantomData}; pub use api_models::{ enums::{Connector, PayoutConnectors}, - payouts as payout_types, + mandates, payouts as payout_types, }; -pub use common_utils::request::RequestBody; +use common_enums::MandateStatus; +pub use common_utils::request::{RequestBody, RequestContent}; use common_utils::{pii, pii::Email}; use data_models::mandates::MandateData; use error_stack::{IntoReport, ResultExt}; use masking::Secret; +use serde::Serialize; use self::{api::payments, storage::enums as storage_enums}; pub use crate::core::payments::{CustomerDetails, PaymentAddress}; @@ -30,9 +36,10 @@ use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLO use crate::{ core::{ errors::{self, RouterResult}, - payments::RecurringMandatePaymentData, + payments::{types, PaymentData, RecurringMandatePaymentData}, }, services, + types::transformers::ForeignFrom, utils::OptionExt, }; @@ -51,6 +58,11 @@ pub type PaymentsBalanceRouterData = pub type PaymentsSyncRouterData = RouterData; pub type PaymentsCaptureRouterData = RouterData; +pub type PaymentsIncrementalAuthorizationRouterData = RouterData< + api::IncrementalAuthorization, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, +>; pub type PaymentsCancelRouterData = RouterData; pub type PaymentsRejectRouterData = RouterData; @@ -106,6 +118,11 @@ pub type SetupMandateType = dyn services::ConnectorIntegration< SetupMandateRequestData, PaymentsResponseData, >; +pub type MandateRevokeType = dyn services::ConnectorIntegration< + api::MandateRevoke, + MandateRevokeRequestData, + MandateRevokeResponseData, +>; pub type PaymentsPreProcessingType = dyn services::ConnectorIntegration< api::PreProcessing, PaymentsPreProcessingData, @@ -141,6 +158,11 @@ pub type TokenizationType = dyn services::ConnectorIntegration< PaymentMethodTokenizationData, PaymentsResponseData, >; +pub type IncrementalAuthorizationType = dyn services::ConnectorIntegration< + api::IncrementalAuthorization, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, +>; pub type ConnectorCustomerType = dyn services::ConnectorIntegration< api::CreateConnectorCustomer, @@ -230,6 +252,9 @@ pub type RetrieveFileRouterData = pub type DefendDisputeRouterData = RouterData; +pub type MandateRevokeRouterData = + RouterData; + #[cfg(feature = "payouts")] pub type PayoutsRouterData = RouterData; @@ -292,6 +317,11 @@ pub struct RouterData { pub external_latency: Option, /// Contains apple pay flow type simplified or manual pub apple_pay_flow: Option, + + pub frm_metadata: Option, + + pub dispute_id: Option, + pub refund_id: Option, } #[derive(Debug, Clone, serde::Deserialize)] @@ -322,7 +352,7 @@ pub struct ApplePayCryptogramData { #[derive(Debug, Clone)] pub struct PaymentMethodBalance { pub amount: i64, - pub currency: String, + pub currency: storage_enums::Currency, } #[cfg(feature = "payouts")] @@ -355,8 +385,17 @@ pub struct PayoutsFulfillResponseData { #[derive(Debug, Clone)] pub struct PaymentsAuthorizeData { pub payment_method_data: payments::PaymentMethodData, + /// total amount (original_amount + surcharge_amount + tax_on_surcharge_amount) + /// If connector supports separate field for surcharge amount, consider using below functions defined on `PaymentsAuthorizeData` to fetch original amount and surcharge amount separately + /// ``` + /// get_original_amount() + /// get_surcharge_amount() + /// get_tax_on_surcharge_amount() + /// get_total_surcharge_amount() // returns surcharge_amount + tax_on_surcharge_amount + /// ``` pub amount: i64, pub email: Option, + pub customer_name: Option>, pub currency: storage_enums::Currency, pub confirm: bool, pub statement_descriptor_suffix: Option, @@ -378,8 +417,10 @@ pub struct PaymentsAuthorizeData { pub related_transaction_id: Option, pub payment_experience: Option, pub payment_method_type: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub customer_id: Option, + pub request_incremental_authorization: bool, + pub metadata: Option, } #[derive(Debug, Clone, Default)] @@ -391,6 +432,17 @@ pub struct PaymentsCaptureData { pub multiple_capture_data: Option, pub connector_meta: Option, pub browser_info: Option, + pub metadata: Option, + // This metadata is used to store the metadata shared during the payment intent request. +} + +#[derive(Debug, Clone, Default)] +pub struct PaymentsIncrementalAuthorizationData { + pub total_amount: i64, + pub additional_amount: i64, + pub currency: storage_enums::Currency, + pub reason: Option, + pub connector_transaction_id: String, } #[allow(dead_code)] @@ -413,7 +465,7 @@ pub struct ConnectorCustomerData { pub description: Option, pub email: Option, pub phone: Option>, - pub name: Option, + pub name: Option>, pub preprocessing_id: Option, pub payment_method_data: payments::PaymentMethodData, } @@ -439,8 +491,10 @@ pub struct PaymentsPreProcessingData { pub router_return_url: Option, pub webhook_url: Option, pub complete_authorize_url: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub browser_info: Option, + pub connector_transaction_id: Option, + pub redirect_response: Option, } #[derive(Debug, Clone)] @@ -461,6 +515,8 @@ pub struct CompleteAuthorizeData { pub browser_info: Option, pub connector_transaction_id: Option, pub connector_meta: Option, + pub complete_authorize_url: Option, + pub metadata: Option, } #[derive(Debug, Clone)] @@ -495,6 +551,8 @@ pub struct PaymentsCancelData { pub cancellation_reason: Option, pub connector_meta: Option, pub browser_info: Option, + pub metadata: Option, + // This metadata is used to store the metadata shared during the payment intent request. } #[derive(Debug, Default, Clone)] @@ -514,7 +572,7 @@ pub struct PaymentsSessionData { pub amount: i64, pub currency: storage_enums::Currency, pub country: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub order_details: Option>, } @@ -532,8 +590,11 @@ pub struct SetupMandateRequestData { pub router_return_url: Option, pub browser_info: Option, pub email: Option, + pub customer_name: Option>, pub return_url: Option, pub payment_method_type: Option, + pub request_incremental_authorization: bool, + pub metadata: Option, } #[derive(Debug, Clone)] @@ -544,54 +605,223 @@ pub struct AccessTokenRequestData { } pub trait Capturable { - fn get_capture_amount(&self) -> Option { - Some(0) - } - fn get_surcharge_amount(&self) -> Option { + fn get_captured_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { None } - fn get_tax_on_surcharge_amount(&self) -> Option { + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + _attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { None } } impl Capturable for PaymentsAuthorizeData { - fn get_capture_amount(&self) -> Option { + fn get_captured_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) + + fn get_amount_capturable( + &self, + payment_data: &PaymentData, + attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + match payment_data + .payment_attempt + .capture_method + .unwrap_or_default() + { + common_enums::CaptureMethod::Automatic => { + let intent_status = common_enums::IntentStatus::foreign_from(attempt_status); + match intent_status { + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Processing => Some(0), + common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + }, + common_enums::CaptureMethod::Manual => Some(payment_data.payment_attempt.get_total_amount()), + // In case of manual multiple, amount capturable must be inferred from all captures. + common_enums::CaptureMethod::ManualMultiple | + // Scheduled capture is not supported as of now + common_enums::CaptureMethod::Scheduled => None, + } } } impl Capturable for PaymentsCaptureData { - fn get_capture_amount(&self) -> Option { + fn get_captured_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { Some(self.amount_to_capture) } + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + let intent_status = common_enums::IntentStatus::foreign_from(attempt_status); + match intent_status { + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::Processing => Some(0), + common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } } impl Capturable for CompleteAuthorizeData { - fn get_capture_amount(&self) -> Option { + fn get_captured_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { Some(self.amount) } + fn get_amount_capturable( + &self, + payment_data: &PaymentData, + attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + match payment_data + .payment_attempt + .capture_method + .unwrap_or_default() + { + common_enums::CaptureMethod::Automatic => { + let intent_status = common_enums::IntentStatus::foreign_from(attempt_status); + match intent_status { + common_enums::IntentStatus::Succeeded| + common_enums::IntentStatus::Failed| + common_enums::IntentStatus::Processing => Some(0), + common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + }, + common_enums::CaptureMethod::Manual => Some(payment_data.payment_attempt.get_total_amount()), + // In case of manual multiple, amount capturable must be inferred from all captures. + common_enums::CaptureMethod::ManualMultiple | + // Scheduled capture is not supported as of now + common_enums::CaptureMethod::Scheduled => None, + } + } } impl Capturable for SetupMandateRequestData {} -impl Capturable for PaymentsCancelData {} +impl Capturable for PaymentsCancelData { + fn get_captured_amount(&self, payment_data: &PaymentData) -> Option + where + F: Clone, + { + // return previously captured amount + payment_data.payment_intent.amount_captured + } + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + let intent_status = common_enums::IntentStatus::foreign_from(attempt_status); + match intent_status { + common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Processing + | common_enums::IntentStatus::PartiallyCaptured => Some(0), + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } +} impl Capturable for PaymentsApproveData {} impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} -impl Capturable for PaymentsSyncData {} +impl Capturable for PaymentsIncrementalAuthorizationData { + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + _attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + Some(self.total_amount) + } +} +impl Capturable for PaymentsSyncData { + fn get_captured_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())) + } + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + if attempt_status.is_terminal_status() { + Some(0) + } else { + None + } + } +} pub struct AddAccessTokenResult { pub access_token_result: Result, ErrorResponse>, @@ -653,6 +883,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, @@ -688,6 +919,12 @@ pub enum PaymentsResponseData { session_token: Option, connector_response_reference_id: Option, }, + IncrementalAuthorizationResponse { + status: common_enums::AuthorizationStatus, + connector_authorization_id: Option, + error_code: Option, + error_message: Option, + }, } #[derive(Debug, Clone)] @@ -696,7 +933,7 @@ pub enum PreprocessingResponseId { ConnectorTransactionId(String), } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize)] pub enum ResponseId { ConnectorTransactionId(String), EncodedData(String), @@ -896,10 +1133,22 @@ pub struct ResponseRouterData { pub http_code: u16, } +#[derive(Debug, Clone)] +pub struct MandateRevokeRequestData { + pub mandate_id: String, + pub connector_mandate_id: Option, +} + +#[derive(Debug, Clone)] +pub struct MandateRevokeResponseData { + pub mandate_status: MandateStatus, +} + // 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 +1174,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 +1265,7 @@ pub struct ErrorResponse { pub reason: Option, pub status_code: u16, pub attempt_status: Option, + pub connector_transaction_id: Option, } impl ErrorResponse { @@ -960,6 +1282,7 @@ impl ErrorResponse { reason: None, status_code: http::StatusCode::INTERNAL_SERVER_ERROR.as_u16(), attempt_status: None, + connector_transaction_id: None, } } } @@ -1003,6 +1326,7 @@ impl From for ErrorResponse { _ => 500, }, attempt_status: None, + connector_transaction_id: None, } } } @@ -1024,19 +1348,6 @@ impl From<&&mut PaymentsAuthorizeRouterData> for AuthorizeSessionTokenData { } } -impl From<&&mut PaymentsAuthorizeRouterData> for ConnectorCustomerData { - fn from(data: &&mut PaymentsAuthorizeRouterData) -> Self { - Self { - email: data.request.email.to_owned(), - preprocessing_id: data.preprocessing_id.to_owned(), - payment_method_data: data.request.payment_method_data.to_owned(), - description: None, - phone: None, - name: None, - } - } -} - impl From<&RouterData> for PaymentMethodTokenizationData { @@ -1093,6 +1404,7 @@ impl From<&SetupMandateRouterData> for PaymentsAuthorizeData { setup_mandate_details: data.request.setup_mandate_details.clone(), router_return_url: data.request.router_return_url.clone(), email: data.request.email.clone(), + customer_name: data.request.customer_name.clone(), amount: 0, statement_descriptor: None, capture_method: None, @@ -1108,6 +1420,8 @@ impl From<&SetupMandateRouterData> for PaymentsAuthorizeData { payment_method_type: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: data.request.request_incremental_authorization, + metadata: None, } } } @@ -1155,6 +1469,9 @@ impl From<(&RouterData, T2)> connector_http_status_code: data.connector_http_status_code, external_latency: data.external_latency, apple_pay_flow: data.apple_pay_flow.clone(), + frm_metadata: data.frm_metadata.clone(), + dispute_id: data.dispute_id.clone(), + refund_id: data.refund_id.clone(), } } } @@ -1210,6 +1527,9 @@ impl connector_http_status_code: data.connector_http_status_code, external_latency: data.external_latency, apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, } } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index dc615c4e41fa..b60153eaf194 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -1,33 +1,44 @@ pub mod admin; pub mod api_keys; pub mod configs; +#[cfg(feature = "olap")] +pub mod connector_onboarding; pub mod customers; pub mod disputes; pub mod enums; pub mod ephemeral_key; pub mod files; +#[cfg(feature = "frm")] +pub mod fraud_check; 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}; -use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "frm")] +pub use self::fraud_check::*; 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::{ configs::settings::Connectors, connector, consts, - core::errors::{self, CustomResult}, + core::{ + errors::{self, CustomResult}, + payments::types as payments_types, + }, services::{request, ConnectorIntegration, ConnectorRedirectResponse, ConnectorValidation}, types::{self, api::enums as api_enums}, }; @@ -59,6 +70,18 @@ pub trait ConnectorVerifyWebhookSource: { } +#[derive(Clone, Debug)] +pub struct MandateRevoke; + +pub trait ConnectorMandateRevoke: + ConnectorIntegration< + MandateRevoke, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, +> +{ +} + pub trait ConnectorTransactionId: ConnectorCommon + Sync { fn connector_transaction_id( &self, @@ -113,6 +136,7 @@ pub trait ConnectorCommon { message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -146,6 +170,8 @@ pub trait Connector: + ConnectorTransactionId + Payouts + ConnectorVerifyWebhookSource + + FraudCheck + + ConnectorMandateRevoke { } @@ -165,7 +191,9 @@ impl< + FileUpload + ConnectorTransactionId + Payouts - + ConnectorVerifyWebhookSource, + + ConnectorVerifyWebhookSource + + FraudCheck + + ConnectorMandateRevoke, > Connector for T { } @@ -218,9 +246,9 @@ pub struct SessionConnectorData { /// Session Surcharge type pub enum SessionSurchargeDetails { /// Surcharge is calculated by hyperswitch - Calculated(SurchargeMetadata), + Calculated(payments_types::SurchargeMetadata), /// Surcharge is sent by merchant - PreDetermined(SurchargeDetailsResponse), + PreDetermined(payments_types::SurchargeDetails), } impl SessionSurchargeDetails { @@ -229,10 +257,14 @@ impl SessionSurchargeDetails { payment_method: &enums::PaymentMethod, payment_method_type: &enums::PaymentMethodType, card_network: Option<&enums::CardNetwork>, - ) -> Option { + ) -> Option { match self { Self::Calculated(surcharge_metadata) => surcharge_metadata - .get_surcharge_details(payment_method, payment_method_type, card_network) + .get_surcharge_details(payments_types::SurchargeKey::PaymentMethodData( + *payment_method, + *payment_method_type, + card_network.cloned(), + )) .cloned(), Self::PreDetermined(surcharge_details) => Some(surcharge_details.clone()), } @@ -370,6 +402,7 @@ impl ConnectorData { // "payeezy" => Ok(Box::new(&connector::Payeezy)), As psync and rsync are not supported by this connector, it is added as template code for future usage enums::Connector::Payme => Ok(Box::new(&connector::Payme)), enums::Connector::Payu => Ok(Box::new(&connector::Payu)), + enums::Connector::Placetopay => Ok(Box::new(&connector::Placetopay)), enums::Connector::Powertranz => Ok(Box::new(&connector::Powertranz)), enums::Connector::Prophetpay => Ok(Box::new(&connector::Prophetpay)), enums::Connector::Rapyd => Ok(Box::new(&connector::Rapyd)), @@ -387,7 +420,9 @@ impl ConnectorData { enums::Connector::Tsys => Ok(Box::new(&connector::Tsys)), enums::Connector::Volt => Ok(Box::new(&connector::Volt)), enums::Connector::Zen => Ok(Box::new(&connector::Zen)), - enums::Connector::Signifyd | enums::Connector::Plaid => { + enums::Connector::Signifyd + | enums::Connector::Plaid + | enums::Connector::Riskified => { Err(report!(errors::ConnectorError::InvalidConnectorName) .attach_printable(format!("invalid connector name: {connector_name}"))) .change_context(errors::ApiErrorResponse::InternalServerError) @@ -400,6 +435,20 @@ impl ConnectorData { } } +#[cfg(feature = "frm")] +pub trait FraudCheck: + ConnectorCommon + + FraudCheckSale + + FraudCheckTransaction + + FraudCheckCheckout + + FraudCheckFulfillment + + FraudCheckRecordReturn +{ +} + +#[cfg(not(feature = "frm"))] +pub trait FraudCheck {} + #[cfg(test)] mod test { #![allow(clippy::unwrap_used)] diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 6bbe9149f4d7..487cea88d150 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -14,6 +14,7 @@ use masking::Secret; use crate::{ core::errors, types::{domain, storage, transformers::ForeignTryFrom}, + utils::{self}, }; impl TryFrom for MerchantAccountResponse { @@ -46,7 +47,6 @@ impl TryFrom for MerchantAccountResponse { is_recon_enabled: item.is_recon_enabled, default_profile: item.default_profile, recon_status: item.recon_status, - payment_link_config: item.payment_link_config, }) } } @@ -72,6 +72,8 @@ impl ForeignTryFrom for BusinessProf frm_routing_algorithm: item.frm_routing_algorithm, payout_routing_algorithm: item.payout_routing_algorithm, applepay_verified_domains: item.applepay_verified_domains, + payment_link_config: item.payment_link_config, + session_expiry: item.session_expiry, }) } } @@ -105,6 +107,18 @@ impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)> .or(merchant_account.payment_response_hash_key) .unwrap_or(common_utils::crypto::generate_cryptographically_secure_random_string(64)); + let payment_link_config_value = request + .payment_link_config + .map(|pl_config| { + utils::Encode::::encode_to_value( + &pl_config, + ) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config_value", + }) + }) + .transpose()?; + Ok(Self { profile_id, merchant_id: merchant_account.merchant_id, @@ -124,9 +138,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) @@ -139,6 +154,11 @@ impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)> .or(merchant_account.payout_routing_algorithm), is_recon_enabled: merchant_account.is_recon_enabled, applepay_verified_domains: request.applepay_verified_domains, + payment_link_config: payment_link_config_value, + session_expiry: request + .session_expiry + .map(i64::from) + .or(Some(common_utils::consts::DEFAULT_SESSION_EXPIRY)), }) } } diff --git a/crates/router/src/types/api/connector_onboarding.rs b/crates/router/src/types/api/connector_onboarding.rs new file mode 100644 index 000000000000..5b1d581a20ef --- /dev/null +++ b/crates/router/src/types/api/connector_onboarding.rs @@ -0,0 +1 @@ +pub mod paypal; diff --git a/crates/router/src/types/api/connector_onboarding/paypal.rs b/crates/router/src/types/api/connector_onboarding/paypal.rs new file mode 100644 index 000000000000..dbfdd6f50075 --- /dev/null +++ b/crates/router/src/types/api/connector_onboarding/paypal.rs @@ -0,0 +1,247 @@ +use api_models::connector_onboarding as api; +use error_stack::{IntoReport, ResultExt}; + +use crate::core::errors::{ApiErrorResponse, RouterResult}; + +#[derive(serde::Deserialize, Debug)] +pub struct HateoasLink { + pub href: String, + pub rel: String, + pub method: String, +} + +#[derive(serde::Deserialize, Debug)] +pub struct PartnerReferralResponse { + pub links: Vec, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralRequest { + pub tracking_id: String, + pub operations: Vec, + pub products: Vec, + pub capabilities: Vec, + pub partner_config_override: PartnerConfigOverride, + pub legal_consents: Vec, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalProducts { + Ppcp, + AdvancedVaulting, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalCapabilities { + PaypalWalletVaultingAdvanced, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralOperations { + pub operation: PayPalReferralOperationType, + pub api_integration_preference: PartnerReferralIntegrationPreference, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalReferralOperationType { + ApiIntegration, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralIntegrationPreference { + pub rest_api_integration: PartnerReferralRestApiIntegration, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralRestApiIntegration { + pub integration_method: IntegrationMethod, + pub integration_type: PayPalIntegrationType, + pub third_party_details: PartnerReferralThirdPartyDetails, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IntegrationMethod { + Paypal, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalIntegrationType { + ThirdParty, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralThirdPartyDetails { + pub features: Vec, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalFeatures { + Payment, + Refund, + Vault, + AccessMerchantInformation, + BillingAgreement, + ReadSellerDispute, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerConfigOverride { + pub partner_logo_url: String, + pub return_url: String, +} + +#[derive(serde::Serialize, Debug)] +pub struct LegalConsent { + #[serde(rename = "type")] + pub consent_type: LegalConsentType, + pub granted: bool, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum LegalConsentType { + ShareDataConsent, +} + +impl PartnerReferralRequest { + pub fn new(tracking_id: String, return_url: String) -> Self { + Self { + tracking_id, + operations: vec![PartnerReferralOperations { + operation: PayPalReferralOperationType::ApiIntegration, + api_integration_preference: PartnerReferralIntegrationPreference { + rest_api_integration: PartnerReferralRestApiIntegration { + integration_method: IntegrationMethod::Paypal, + integration_type: PayPalIntegrationType::ThirdParty, + third_party_details: PartnerReferralThirdPartyDetails { + features: vec![ + PayPalFeatures::Payment, + PayPalFeatures::Refund, + PayPalFeatures::Vault, + PayPalFeatures::AccessMerchantInformation, + PayPalFeatures::BillingAgreement, + PayPalFeatures::ReadSellerDispute, + ], + }, + }, + }, + }], + products: vec![PayPalProducts::Ppcp, PayPalProducts::AdvancedVaulting], + capabilities: vec![PayPalCapabilities::PaypalWalletVaultingAdvanced], + partner_config_override: PartnerConfigOverride { + partner_logo_url: "https://hyperswitch.io/img/websiteIcon.svg".to_string(), + return_url, + }, + legal_consents: vec![LegalConsent { + consent_type: LegalConsentType::ShareDataConsent, + granted: true, + }], + } + } +} + +#[derive(serde::Deserialize, Debug)] +pub struct SellerStatusResponse { + pub merchant_id: String, + pub links: Vec, +} + +#[derive(serde::Deserialize, Debug)] +pub struct SellerStatusDetailsResponse { + pub merchant_id: String, + pub primary_email_confirmed: bool, + pub payments_receivable: bool, + pub products: Vec, +} + +#[derive(serde::Deserialize, Debug)] +pub struct SellerStatusProducts { + pub name: String, + pub vetting_status: Option, +} + +#[derive(serde::Deserialize, Debug, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VettingStatus { + NeedMoreData, + Subscribed, + Denied, +} + +impl SellerStatusResponse { + pub fn extract_merchant_details_url(self, paypal_base_url: &str) -> RouterResult { + self.links + .first() + .and_then(|link| link.href.strip_prefix('/')) + .map(|link| format!("{}{}", paypal_base_url, link)) + .ok_or(ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Merchant details not received in onboarding status") + } +} + +impl SellerStatusDetailsResponse { + pub fn check_payments_receivable(&self) -> Option { + if !self.payments_receivable { + return Some(api::PayPalOnboardingStatus::PaymentsNotReceivable); + } + None + } + + pub fn check_ppcp_custom_status(&self) -> Option { + match self.get_ppcp_custom_status() { + Some(VettingStatus::Denied) => Some(api::PayPalOnboardingStatus::PpcpCustomDenied), + Some(VettingStatus::Subscribed) => None, + _ => Some(api::PayPalOnboardingStatus::MorePermissionsNeeded), + } + } + + fn check_email_confirmation(&self) -> Option { + if !self.primary_email_confirmed { + return Some(api::PayPalOnboardingStatus::EmailNotVerified); + } + None + } + + pub async fn get_eligibility_status(&self) -> RouterResult { + Ok(self + .check_payments_receivable() + .or(self.check_email_confirmation()) + .or(self.check_ppcp_custom_status()) + .unwrap_or(api::PayPalOnboardingStatus::Success( + api::PayPalOnboardingDone { + payer_id: self.get_payer_id(), + }, + ))) + } + + fn get_ppcp_custom_status(&self) -> Option { + self.products + .iter() + .find(|product| product.name == "PPCP_CUSTOM") + .and_then(|ppcp_custom| ppcp_custom.vetting_status.clone()) + } + + fn get_payer_id(&self) -> String { + self.merchant_id.to_string() + } +} + +impl PartnerReferralResponse { + pub fn extract_action_url(self) -> RouterResult { + Ok(self + .links + .into_iter() + .find(|hateoas_link| hateoas_link.rel == "action_url") + .ok_or(ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Failed to get action_url from paypal response")? + .href) + } +} diff --git a/crates/router/src/types/api/fraud_check.rs b/crates/router/src/types/api/fraud_check.rs new file mode 100644 index 000000000000..e871cbc5d330 --- /dev/null +++ b/crates/router/src/types/api/fraud_check.rs @@ -0,0 +1,92 @@ +use std::str::FromStr; + +use api_models::enums; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; + +use super::{BoxedConnector, ConnectorData, SessionConnectorData}; +use crate::{ + connector, + core::errors, + services::api, + types::fraud_check::{ + FraudCheckCheckoutData, FraudCheckFulfillmentData, FraudCheckRecordReturnData, + FraudCheckResponseData, FraudCheckSaleData, FraudCheckTransactionData, + }, +}; + +#[derive(Debug, Clone)] +pub struct Sale; + +pub trait FraudCheckSale: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct Checkout; + +pub trait FraudCheckCheckout: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct Transaction; + +pub trait FraudCheckTransaction: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct Fulfillment; + +pub trait FraudCheckFulfillment: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct RecordReturn; + +pub trait FraudCheckRecordReturn: + api::ConnectorIntegration +{ +} + +#[derive(Clone, Debug)] +pub struct FraudCheckConnectorData { + pub connector: BoxedConnector, + pub connector_name: enums::FrmConnectors, +} +pub enum ConnectorCallType { + PreDetermined(ConnectorData), + Retryable(Vec), + SessionMultiple(Vec), +} + +impl FraudCheckConnectorData { + pub fn get_connector_by_name(name: &str) -> CustomResult { + let connector_name = enums::FrmConnectors::from_str(name) + .into_report() + .change_context(errors::ApiErrorResponse::IncorrectConnectorNameGiven) + .attach_printable_lazy(|| { + format!("unable to parse connector: {:?}", name.to_string()) + })?; + let connector = Self::convert_connector(connector_name)?; + Ok(Self { + connector, + connector_name, + }) + } + + fn convert_connector( + connector_name: enums::FrmConnectors, + ) -> CustomResult { + match connector_name { + enums::FrmConnectors::Signifyd => Ok(Box::new(&connector::Signifyd)), + enums::FrmConnectors::Riskified => Ok(Box::new(&connector::Riskified)), + } + } +} diff --git a/crates/router/src/types/api/mandates.rs b/crates/router/src/types/api/mandates.rs index 8d6b9a15e3a8..f6b2d7bba933 100644 --- a/crates/router/src/types/api/mandates.rs +++ b/crates/router/src/types/api/mandates.rs @@ -1,6 +1,7 @@ use api_models::mandates; pub use api_models::mandates::{MandateId, MandateResponse, MandateRevokedResponse}; use error_stack::ResultExt; +use masking::PeekInterface; use serde::{Deserialize, Serialize}; use crate::{ @@ -11,7 +12,7 @@ use crate::{ newtype, routes::AppState, types::{ - api, + api, domain, storage::{self, enums as storage_enums}, }, }; @@ -23,12 +24,20 @@ newtype!( #[async_trait::async_trait] pub(crate) trait MandateResponseExt: Sized { - async fn from_db_mandate(state: &AppState, mandate: storage::Mandate) -> RouterResult; + async fn from_db_mandate( + state: &AppState, + key_store: domain::MerchantKeyStore, + mandate: storage::Mandate, + ) -> RouterResult; } #[async_trait::async_trait] impl MandateResponseExt for MandateResponse { - async fn from_db_mandate(state: &AppState, mandate: storage::Mandate) -> RouterResult { + async fn from_db_mandate( + state: &AppState, + key_store: domain::MerchantKeyStore, + mandate: storage::Mandate, + ) -> RouterResult { let db = &*state.store; let payment_method = db .find_payment_method(&mandate.payment_method_id) @@ -36,21 +45,35 @@ impl MandateResponseExt for MandateResponse { .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; let card = if payment_method.payment_method == storage_enums::PaymentMethod::Card { - let card = payment_methods::cards::get_card_from_locker( - state, - &payment_method.customer_id, - &payment_method.merchant_id, - &payment_method.payment_method_id, - ) - .await?; - let card_detail = payment_methods::transformers::get_card_detail(&payment_method, card) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting card details")?; - Some(MandateCardDetails::from(card_detail).into_inner()) + // if locker is disabled , decrypt the payment method data + let card_details = if state.conf.locker.locker_enabled { + let card = payment_methods::cards::get_card_from_locker( + state, + &payment_method.customer_id, + &payment_method.merchant_id, + &payment_method.payment_method_id, + ) + .await?; + + payment_methods::transformers::get_card_detail(&payment_method, card) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting card details")? + } else { + payment_methods::cards::get_card_details_without_locker_fallback( + &payment_method, + key_store.key.get_inner().peek(), + state, + ) + .await? + }; + + Some(MandateCardDetails::from(card_details).into_inner()) } else { None }; - + let payment_method_type = payment_method + .payment_method_type + .map(|pmt| pmt.to_string()); Ok(Self { mandate_id: mandate.mandate_id, customer_acceptance: Some(api::payments::CustomerAcceptance { @@ -68,6 +91,7 @@ impl MandateResponseExt for MandateResponse { card, status: mandate.mandate_status, payment_method: payment_method.payment_method.to_string(), + payment_method_type, payment_method_id: mandate.payment_method_id, }) } @@ -84,6 +108,10 @@ impl From for MandateCardDetails { scheme: card_details_from_locker.scheme, issuer_country: card_details_from_locker.issuer_country, card_fingerprint: card_details_from_locker.card_fingerprint, + card_isin: card_details_from_locker.card_isin, + card_issuer: card_details_from_locker.card_issuer, + card_network: card_details_from_locker.card_network, + card_type: card_details_from_locker.card_type, } .into() } 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..85cb539d4118 --- /dev/null +++ b/crates/router/src/types/api/payment_link.rs @@ -0,0 +1,35 @@ +pub use api_models::payments::RetrievePaymentLinkResponse; + +use crate::{ + consts::DEFAULT_SESSION_EXPIRY, + 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 session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| { + payment_link + .created_at + .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY)) + }); + let status = payment_link::check_payment_link_status(session_expiry); + 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, + expiry: payment_link.fulfilment_time, + currency: payment_link.currency, + status, + }) + } +} diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index 5acb66b5068e..ca852f832ee8 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,4 +1,3 @@ -use api_models::enums as api_enums; pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, @@ -9,9 +8,9 @@ pub use api_models::payment_methods::{ }; use error_stack::report; -use crate::{ - core::errors::{self, RouterResult}, - types::transformers::ForeignFrom, +use crate::core::{ + errors::{self, RouterResult}, + payments::helpers::validate_payment_method_type_against_payment_method, }; pub(crate) trait PaymentMethodCreateExt { @@ -21,16 +20,16 @@ pub(crate) trait PaymentMethodCreateExt { // convert self.payment_method_type to payment_method and compare it against self.payment_method impl PaymentMethodCreateExt for PaymentMethodCreate { fn validate(&self) -> RouterResult<()> { - let payment_method: Option = - self.payment_method_type.map(ForeignFrom::foreign_from); - if payment_method - .map(|payment_method| payment_method != self.payment_method) - .unwrap_or(false) - { - return Err(report!(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid 'payment_method_type' provided".to_string() - }) - .attach_printable("Invalid payment method type")); + if let Some(payment_method_type) = self.payment_method_type { + if !validate_payment_method_type_against_payment_method( + self.payment_method, + payment_method_type, + ) { + return Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid 'payment_method_type' provided".to_string() + }) + .attach_printable("Invalid payment method type")); + } } Ok(()) } diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index b00a7f0cbdac..9159abf4bd1c 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -5,11 +5,12 @@ pub use api_models::payments::{ PayLaterData, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListResponse, PaymentListResponseV2, PaymentMethodData, PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, - PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsRedirectRequest, - PaymentsRedirectionResponse, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, - PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, - PaymentsStartRequest, PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, - TimeRange, UrlDetails, VerifyRequest, VerifyResponse, WalletData, + PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, + PaymentsIncrementalAuthorizationRequest, PaymentsRedirectRequest, PaymentsRedirectionResponse, + PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, + PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, + PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, TimeRange, UrlDetails, + VerifyRequest, VerifyResponse, WalletData, }; use error_stack::{IntoReport, ResultExt}; @@ -81,6 +82,9 @@ pub struct SetupMandate; #[derive(Debug, Clone)] pub struct PreProcessing; +#[derive(Debug, Clone)] +pub struct IncrementalAuthorization; + pub trait PaymentIdTypeExt { fn get_payment_intent_id(&self) -> errors::CustomResult; } @@ -164,6 +168,15 @@ pub trait MandateSetup: { } +pub trait PaymentIncrementalAuthorization: + api::ConnectorIntegration< + IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, +> +{ +} + pub trait PaymentsCompleteAuthorize: api::ConnectorIntegration< CompleteAuthorize, @@ -215,6 +228,7 @@ pub trait Payment: + PaymentToken + PaymentsPreProcessing + ConnectorCustomer + + PaymentIncrementalAuthorization { } @@ -230,7 +244,7 @@ mod payments_test { card_number: "1234432112344321".to_string().try_into().unwrap(), card_exp_month: "12".to_string().into(), card_exp_year: "99".to_string().into(), - card_holder_name: "JohnDoe".to_string().into(), + card_holder_name: Some(masking::Secret::new("JohnDoe".to_string())), card_cvc: "123".to_string().into(), card_issuer: Some("HDFC".to_string()), card_network: Some(api_models::enums::CardNetwork::Visa), 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..c03f492e8b8a --- /dev/null +++ b/crates/router/src/types/api/verify_connector.rs @@ -0,0 +1,187 @@ +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, + customer_name: None, + amount: 1000, + confirm: true, + currency: storage_enums::Currency::USD, + metadata: None, + 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, + frm_metadata: None, + refund_id: None, + dispute_id: 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/merchant_account.rs b/crates/router/src/types/domain/merchant_account.rs index cbcd70aaef17..3832ffb3da7e 100644 --- a/crates/router/src/types/domain/merchant_account.rs +++ b/crates/router/src/types/domain/merchant_account.rs @@ -79,6 +79,7 @@ pub enum MerchantAccountUpdate { recon_status: diesel_models::enums::ReconStatus, }, UnsetDefaultProfile, + ModifiedAtUpdate, } impl From for MerchantAccountUpdateInternal { @@ -140,6 +141,10 @@ impl From for MerchantAccountUpdateInternal { default_profile: Some(None), ..Default::default() }, + MerchantAccountUpdate::ModifiedAtUpdate => Self { + modified_at: Some(date_time::now()), + ..Default::default() + }, } } } 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..d3ea69ecd863 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,28 @@ 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 as auth, + authentication::UserFromToken, + authorization::{info, predefined_permissions}, + }, types::transformers::ForeignFrom, - utils::user::password, + utils::{self, user::password}, }; +pub mod dashboard_metadata; + #[derive(Clone)] pub struct UserName(Secret); @@ -34,7 +43,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 +174,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() @@ -206,6 +216,25 @@ impl NewUserOrganization { } } +impl TryFrom for NewUserOrganization { + type Error = error_stack::Report; + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let new_organization = api_org::OrganizationNew::new(Some( + UserCompanyName::new(value.company_name)?.get_secret(), + )); + let db_organization = ForeignFrom::foreign_from(new_organization); + Ok(Self(db_organization)) + } +} + +impl From for NewUserOrganization { + fn from(_value: user_api::SignUpRequest) -> 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: user_api::ConnectAccountRequest) -> Self { let new_organization = api_org::OrganizationNew::new(None); @@ -214,9 +243,56 @@ 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), + }) + } +} + +type InviteeUserRequestWithInvitedUserToken = (user_api::InviteUserRequest, UserFromToken); +impl From for NewUserOrganization { + fn from(_value: InviteeUserRequestWithInvitedUserToken) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + +#[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 +303,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 { @@ -269,10 +345,8 @@ impl NewUserMerchant { merchant_details: None, routing_algorithm: None, parent_merchant_id: None, - payment_link_config: None, sub_merchants_enabled: None, frm_routing_algorithm: None, - intent_fulfillment_time: None, payout_routing_algorithm: None, primary_business_details: None, payment_response_hash_key: None, @@ -287,11 +361,63 @@ impl NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpRequest) -> UserResult { + 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::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::SignUpWithMerchantIdRequest) -> UserResult { + let company_name = Some(UserCompanyName::new(value.company_name.clone())?); + let merchant_id = MerchantId::new(value.company_name.clone())?; + let new_organization = NewUserOrganization::try_from(value)?; + + Ok(Self { + company_name, + 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 +428,42 @@ impl TryFrom for NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + fn try_from(value: InviteeUserRequestWithInvitedUserToken) -> UserResult { + let merchant_id = MerchantId::new(value.clone().1.merchant_id)?; + let new_organization = NewUserOrganization::from(value); + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + +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, @@ -328,6 +490,10 @@ impl NewUser { self.new_merchant.clone() } + pub fn get_password(&self) -> UserPassword { + self.password.clone() + } + pub async fn insert_user_in_db( &self, db: &dyn StorageInterface, @@ -345,10 +511,23 @@ impl NewUser { .attach_printable("Error while inserting user") } + pub async fn check_if_already_exists_in_db(&self, state: AppState) -> UserResult<()> { + if state + .store + .find_user_by_email(self.get_email().into_inner().expose().expose().as_str()) + .await + .is_ok() + { + return Err(UserErrors::UserExists).into_report(); + } + Ok(()) + } + pub async fn insert_user_and_merchant_in_db( &self, state: AppState, ) -> UserResult { + self.check_if_already_exists_in_db(state.clone()).await?; let db = state.store.as_ref(); let merchant_id = self.get_new_merchant().get_merchant_id(); self.new_merchant @@ -380,7 +559,7 @@ impl NewUser { user_id, role_id, created_at: now, - last_modified_at: now, + last_modified: now, org_id: self .get_new_merchant() .get_new_organization() @@ -406,6 +585,46 @@ impl TryFrom for storage_user::UserNew { } } +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let email = value.email.clone().try_into()?; + let name = UserName::new(value.name.clone())?; + let password = UserPassword::new(value.password.clone())?; + let user_id = uuid::Uuid::new_v4().to_string(); + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + name, + email, + password, + user_id, + new_merchant, + }) + } +} + +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpRequest) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.email.clone().try_into()?; + let name = UserName::try_from(value.email.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; @@ -413,6 +632,26 @@ impl TryFrom for NewUser { let user_id = uuid::Uuid::new_v4().to_string(); let email = value.email.clone().try_into()?; let name = UserName::try_from(value.email.clone())?; + let password = UserPassword::new(uuid::Uuid::new_v4().to_string().into())?; + 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: 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)?; @@ -426,6 +665,43 @@ impl TryFrom for NewUser { } } +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, + }) + } +} + +impl TryFrom for NewUser { + type Error = error_stack::Report; + fn try_from(value: InviteeUserRequestWithInvitedUserToken) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.0.email.clone().try_into()?; + let name = UserName::new(value.0.name.clone())?; + let password = UserPassword::new(uuid::Uuid::new_v4().to_string().into())?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + +#[derive(Clone)] pub struct UserFromStorage(pub storage_user::User); impl From for UserFromStorage { @@ -455,29 +731,230 @@ impl UserFromStorage { self.0.email.clone() } - pub async fn get_jwt_auth_token(&self, state: AppState, org_id: String) -> UserResult { - let role_id = self.get_role_from_db(state.clone()).await?.role_id; - let merchant_id = state + pub async fn get_role_from_db(&self, state: AppState) -> UserResult { + state .store - .find_user_role_by_user_id(self.get_user_id()) + .find_user_role_by_user_id(&self.0.user_id) .await - .change_context(UserErrors::InternalServerError)? - .merchant_id; - AuthToken::new_token( - self.0.user_id.clone(), - merchant_id, - role_id, - &state.conf, - org_id, - ) - .await + .change_context(UserErrors::InternalServerError) } - pub async fn get_role_from_db(&self, state: AppState) -> UserResult { + pub async fn get_roles_from_db(&self, state: &AppState) -> UserResult> { state .store - .find_user_role_by_user_id(self.get_user_id()) + .list_user_roles_by_user_id(&self.0.user_id) .await .change_context(UserErrors::InternalServerError) } + + #[cfg(feature = "email")] + pub fn get_verification_days_left(&self, state: &AppState) -> UserResult> { + if self.0.is_verified { + return Ok(None); + } + + let allowed_unverified_duration = + time::Duration::days(state.conf.email.allowed_unverified_days); + + let user_created = self.0.created_at.date(); + let last_date_for_verification = user_created + .checked_add(allowed_unverified_duration) + .ok_or(UserErrors::InternalServerError)?; + + let today = common_utils::date_time::now().date(); + if today >= last_date_for_verification { + return Err(UserErrors::UnverifiedUser.into()); + } + + let days_left_for_verification = last_date_for_verification - today; + Ok(Some(days_left_for_verification.whole_days())) + } + + pub fn get_preferred_merchant_id(&self) -> Option { + self.0.preferred_merchant_id.clone() + } + + pub async fn get_role_from_db_by_merchant_id( + &self, + state: &AppState, + merchant_id: &str, + ) -> UserResult { + state + .store + .find_user_role_by_user_id_merchant_id(self.get_user_id(), merchant_id) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + UserErrors::RoleNotFound + } else { + UserErrors::InternalServerError + } + }) + .into_report() + } +} + +impl From for user_role_api::ModuleInfo { + fn from(value: info::ModuleInfo) -> Self { + Self { + module: value.module.into(), + description: value.description, + permissions: value.permissions.into_iter().map(Into::into).collect(), + } + } +} + +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::Customer => Self::Customer, + info::PermissionModule::Disputes => Self::Disputes, + info::PermissionModule::Files => Self::Files, + info::PermissionModule::ThreeDsDecisionManager => Self::ThreeDsDecisionManager, + info::PermissionModule::SurchargeDecisionManager => Self::SurchargeDecisionManager, + info::PermissionModule::AccountCreate => Self::AccountCreate, + } + } +} + +impl From for user_role_api::PermissionInfo { + fn from(value: info::PermissionInfo) -> Self { + Self { + enum_name: value.enum_name.into(), + description: value.description, + } + } +} + +pub struct UserAndRoleJoined(pub storage_user::User, pub UserRole); + +impl TryFrom for user_api::UserDetails { + type Error = (); + fn try_from(user_and_role: UserAndRoleJoined) -> Result { + let status = match user_and_role.1.status { + UserStatus::Active => user_role_api::UserStatus::Active, + UserStatus::InvitationSent => user_role_api::UserStatus::InvitationSent, + }; + + let role_id = user_and_role.1.role_id; + let role_name = predefined_permissions::get_role_name_from_id(role_id.as_str()) + .ok_or(())? + .to_string(); + + Ok(Self { + user_id: user_and_role.0.user_id, + email: user_and_role.0.email, + name: user_and_role.0.name, + role_id, + status, + role_name, + last_modified_at: user_and_role.0.last_modified_at, + }) + } +} + +pub enum SignInWithRoleStrategyType { + SingleRole(SignInWithSingleRoleStrategy), + MultipleRoles(SignInWithMultipleRolesStrategy), +} + +impl SignInWithRoleStrategyType { + pub async fn decide_signin_strategy_by_user_roles( + user: UserFromStorage, + user_roles: Vec, + ) -> UserResult { + if user_roles.is_empty() { + return Err(UserErrors::InternalServerError.into()); + } + + if let Some(user_role) = user_roles + .iter() + .find(|role| role.status == UserStatus::Active) + { + Ok(Self::SingleRole(SignInWithSingleRoleStrategy { + user, + user_role: user_role.clone(), + })) + } else { + Ok(Self::MultipleRoles(SignInWithMultipleRolesStrategy { + user, + user_roles, + })) + } + } + + pub async fn get_signin_response( + self, + state: &AppState, + ) -> UserResult { + match self { + Self::SingleRole(strategy) => strategy.get_signin_response(state).await, + Self::MultipleRoles(strategy) => strategy.get_signin_response(state).await, + } + } +} + +pub struct SignInWithSingleRoleStrategy { + pub user: UserFromStorage, + pub user_role: UserRole, +} + +impl SignInWithSingleRoleStrategy { + async fn get_signin_response(self, state: &AppState) -> UserResult { + let token = + utils::user::generate_jwt_auth_token(state, &self.user, &self.user_role).await?; + let dashboard_entry_response = + utils::user::get_dashboard_entry_response(state, self.user, self.user_role, token)?; + Ok(user_api::SignInResponse::DashboardEntry( + dashboard_entry_response, + )) + } +} + +pub struct SignInWithMultipleRolesStrategy { + pub user: UserFromStorage, + pub user_roles: Vec, +} + +impl SignInWithMultipleRolesStrategy { + async fn get_signin_response(self, state: &AppState) -> UserResult { + let merchant_accounts = state + .store + .list_multiple_merchant_accounts( + self.user_roles + .iter() + .map(|role| role.merchant_id.clone()) + .collect(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + let merchant_details = utils::user::get_multiple_merchant_details_with_status( + self.user_roles, + merchant_accounts, + )?; + + Ok(user_api::SignInResponse::MerchantSelect( + user_api::MerchantSelectResponse { + name: self.user.get_name(), + email: self.user.get_email(), + token: auth::UserAuthToken::new_token( + self.user.get_user_id().to_string(), + &state.conf, + ) + .await? + .into(), + merchants: merchant_details, + verification_days_left: utils::user::get_verification_days_left(state, &self.user)?, + }, + )) + } } 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..5e4017a3cb1a --- /dev/null +++ b/crates/router/src/types/domain/user/dashboard_metadata.rs @@ -0,0 +1,62 @@ +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), + ConfigurationType(api::ConfigurationType), + IntegrationCompleted(bool), + StripeConnected(api::ProcessorConnected), + PaypalConnected(api::ProcessorConnected), + SPRoutingConfigured(api::ConfiguredRouting), + Feedback(api::Feedback), + ProdIntent(api::ProdIntent), + 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::ConfigurationType(_) => Self::ConfigurationType, + MetaData::IntegrationCompleted(_) => Self::IntegrationCompleted, + MetaData::StripeConnected(_) => Self::StripeConnected, + MetaData::PaypalConnected(_) => Self::PaypalConnected, + MetaData::SPRoutingConfigured(_) => Self::SpRoutingConfigured, + MetaData::Feedback(_) => Self::Feedback, + MetaData::ProdIntent(_) => Self::ProdIntent, + 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/fraud_check.rs b/crates/router/src/types/fraud_check.rs new file mode 100644 index 000000000000..191e74422f91 --- /dev/null +++ b/crates/router/src/types/fraud_check.rs @@ -0,0 +1,138 @@ +use common_utils::pii::Email; + +use crate::{ + connector::signifyd::transformers::RefundMethod, + core::fraud_check::types::FrmFulfillmentRequest, + pii::Serialize, + services, + types::{self, api, storage_enums, ErrorResponse, ResponseId, RouterData}, +}; + +pub type FrmSaleRouterData = RouterData; + +pub type FrmSaleType = + dyn services::ConnectorIntegration; + +#[derive(Debug, Clone)] +pub struct FraudCheckSaleData { + pub amount: i64, + pub order_details: Option>, +} +#[derive(Debug, Clone)] +pub struct FrmRouterData { + pub merchant_id: String, + pub connector: String, + pub payment_id: String, + pub attempt_id: String, + pub request: FrmRequest, + pub response: FrmResponse, +} +#[derive(Debug, Clone)] +pub enum FrmRequest { + Sale(FraudCheckSaleData), + Checkout(FraudCheckCheckoutData), + Transaction(FraudCheckTransactionData), + Fulfillment(FraudCheckFulfillmentData), + RecordReturn(FraudCheckRecordReturnData), +} +#[derive(Debug, Clone)] +pub enum FrmResponse { + Sale(Result), + Checkout(Result), + Transaction(Result), + Fulfillment(Result), + RecordReturn(Result), +} + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +pub enum FraudCheckResponseData { + TransactionResponse { + resource_id: ResponseId, + status: storage_enums::FraudCheckStatus, + connector_metadata: Option, + reason: Option, + score: Option, + }, + FulfillmentResponse { + order_id: String, + shipment_ids: Vec, + }, + RecordReturnResponse { + resource_id: ResponseId, + connector_metadata: Option, + return_id: Option, + }, +} + +pub type FrmCheckoutRouterData = + RouterData; + +pub type FrmCheckoutType = dyn services::ConnectorIntegration< + api::Checkout, + FraudCheckCheckoutData, + FraudCheckResponseData, +>; + +#[derive(Debug, Clone)] +pub struct FraudCheckCheckoutData { + pub amount: i64, + pub order_details: Option>, + pub currency: Option, + pub browser_info: Option, + pub payment_method_data: Option, + pub email: Option, + pub gateway: Option, +} + +pub type FrmTransactionRouterData = + RouterData; + +pub type FrmTransactionType = dyn services::ConnectorIntegration< + api::Transaction, + FraudCheckTransactionData, + FraudCheckResponseData, +>; + +#[derive(Debug, Clone)] +pub struct FraudCheckTransactionData { + pub amount: i64, + pub order_details: Option>, + pub currency: Option, + pub payment_method: Option, + pub error_code: Option, + pub error_message: Option, + pub connector_transaction_id: Option, +} + +pub type FrmFulfillmentRouterData = + RouterData; + +pub type FrmFulfillmentType = dyn services::ConnectorIntegration< + api::Fulfillment, + FraudCheckFulfillmentData, + FraudCheckResponseData, +>; +pub type FrmRecordReturnRouterData = + RouterData; + +pub type FrmRecordReturnType = dyn services::ConnectorIntegration< + api::RecordReturn, + FraudCheckRecordReturnData, + FraudCheckResponseData, +>; + +#[derive(Debug, Clone)] +pub struct FraudCheckFulfillmentData { + pub amount: i64, + pub order_details: Option>>, + pub fulfillment_req: FrmFulfillmentRequest, +} + +#[derive(Debug, Clone)] +pub struct FraudCheckRecordReturnData { + pub amount: i64, + pub currency: Option, + pub refund_method: RefundMethod, + pub refund_transaction_id: Option, +} diff --git a/crates/router/src/types/pm_auth.rs b/crates/router/src/types/pm_auth.rs new file mode 100644 index 000000000000..e2d08c6afeac --- /dev/null +++ b/crates/router/src/types/pm_auth.rs @@ -0,0 +1,38 @@ +use std::str::FromStr; + +use error_stack::{IntoReport, ResultExt}; +use pm_auth::{ + connector::plaid, + types::{ + self as pm_auth_types, + api::{BoxedPaymentAuthConnector, PaymentAuthConnectorData}, + }, +}; + +use crate::core::{ + errors::{self, ApiErrorResponse}, + pm_auth::helpers::PaymentAuthConnectorDataExt, +}; + +impl PaymentAuthConnectorDataExt for PaymentAuthConnectorData { + fn get_connector_by_name(name: &str) -> errors::CustomResult { + let connector_name = pm_auth_types::PaymentMethodAuthConnectors::from_str(name) + .into_report() + .change_context(ApiErrorResponse::IncorrectConnectorNameGiven) + .attach_printable_lazy(|| { + format!("unable to parse connector: {:?}", name.to_string()) + })?; + let connector = Self::convert_connector(connector_name.clone())?; + Ok(Self { + connector, + connector_name, + }) + } + fn convert_connector( + connector_name: pm_auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + match connector_name { + pm_auth_types::PaymentMethodAuthConnectors::Plaid => Ok(Box::new(&plaid::Plaid)), + } + } +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index e3e19323357b..b93cbbbbba92 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -1,15 +1,21 @@ pub mod address; pub mod api_keys; +pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; +pub mod blocklist_lookup; pub mod business_profile; 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; pub mod events; pub mod file; +pub mod fraud_check; pub mod gsm; #[cfg(feature = "kv_store")] pub mod kv; @@ -21,32 +27,31 @@ pub mod merchant_key_store; pub mod payment_attempt; pub mod payment_link; pub mod payment_method; -pub mod routing_algorithm; -use std::collections::HashMap; - -pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate}; -pub use scheduler::db::process_tracker; -pub mod reverse_lookup; - pub mod payout_attempt; pub mod payouts; -mod query; pub mod refund; +pub mod reverse_lookup; +pub mod routing_algorithm; pub mod user; pub mod user_role; +use std::collections::HashMap; + pub use data_models::payments::{ payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, payment_intent::{PaymentIntentNew, PaymentIntentUpdate}, PaymentIntent, }; +pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate}; +pub use scheduler::db::process_tracker; 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::*, authorization::*, blocklist::*, blocklist_fingerprint::*, + blocklist_lookup::*, capture::*, cards_info::*, configs::*, customers::*, + dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, fraud_check::*, + 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/authorization.rs b/crates/router/src/types/storage/authorization.rs new file mode 100644 index 000000000000..678cd64f8810 --- /dev/null +++ b/crates/router/src/types/storage/authorization.rs @@ -0,0 +1 @@ +pub use diesel_models::authorization::{Authorization, AuthorizationNew, AuthorizationUpdate}; diff --git a/crates/router/src/types/storage/blocklist.rs b/crates/router/src/types/storage/blocklist.rs new file mode 100644 index 000000000000..7e7648dd4a08 --- /dev/null +++ b/crates/router/src/types/storage/blocklist.rs @@ -0,0 +1 @@ +pub use diesel_models::blocklist::{Blocklist, BlocklistNew}; diff --git a/crates/router/src/types/storage/blocklist_fingerprint.rs b/crates/router/src/types/storage/blocklist_fingerprint.rs new file mode 100644 index 000000000000..092d881e3fae --- /dev/null +++ b/crates/router/src/types/storage/blocklist_fingerprint.rs @@ -0,0 +1 @@ +pub use diesel_models::blocklist_fingerprint::{BlocklistFingerprint, BlocklistFingerprintNew}; diff --git a/crates/router/src/types/storage/blocklist_lookup.rs b/crates/router/src/types/storage/blocklist_lookup.rs new file mode 100644 index 000000000000..978708ff7c33 --- /dev/null +++ b/crates/router/src/types/storage/blocklist_lookup.rs @@ -0,0 +1 @@ +pub use diesel_models::blocklist_lookup::{BlocklistLookup, BlocklistLookupNew}; 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/fraud_check.rs b/crates/router/src/types/storage/fraud_check.rs new file mode 100644 index 000000000000..f3dd259c3ce4 --- /dev/null +++ b/crates/router/src/types/storage/fraud_check.rs @@ -0,0 +1,3 @@ +pub use diesel_models::fraud_check::{ + FraudCheck, FraudCheckNew, FraudCheckUpdate, FraudCheckUpdateInternal, +}; 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/storage/query.rs b/crates/router/src/types/storage/query.rs deleted file mode 100644 index 1483dec3eab3..000000000000 --- a/crates/router/src/types/storage/query.rs +++ /dev/null @@ -1 +0,0 @@ -pub use diesel_models::query::*; diff --git a/crates/router/src/types/storage/refund.rs b/crates/router/src/types/storage/refund.rs index 4d5667700122..bb05233173c8 100644 --- a/crates/router/src/types/storage/refund.rs +++ b/crates/router/src/types/storage/refund.rs @@ -50,23 +50,40 @@ impl RefundDbExt for Refund { .filter(dsl::merchant_id.eq(merchant_id.to_owned())) .order(dsl::modified_at.desc()) .into_boxed(); - - match &refund_list_details.payment_id { - Some(pid) => { - filter = filter.filter(dsl::payment_id.eq(pid.to_owned())); - } - None => { - filter = filter.limit(limit).offset(offset); - } - }; - match &refund_list_details.refund_id { - Some(ref_id) => { - filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); - } - None => { - filter = filter.limit(limit).offset(offset); - } + let mut search_by_pay_or_ref_id = false; + + if let (Some(pid), Some(ref_id)) = ( + &refund_list_details.payment_id, + &refund_list_details.refund_id, + ) { + search_by_pay_or_ref_id = true; + filter = filter + .filter(dsl::payment_id.eq(pid.to_owned())) + .or_filter(dsl::refund_id.eq(ref_id.to_owned())) + .limit(limit) + .offset(offset); }; + + if !search_by_pay_or_ref_id { + match &refund_list_details.payment_id { + Some(pid) => { + filter = filter.filter(dsl::payment_id.eq(pid.to_owned())); + } + None => { + filter = filter.limit(limit).offset(offset); + } + }; + } + if !search_by_pay_or_ref_id { + match &refund_list_details.refund_id { + Some(ref_id) => { + filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); + } + None => { + filter = filter.limit(limit).offset(offset); + } + }; + } match &refund_list_details.profile_id { Some(profile_id) => { filter = filter @@ -163,7 +180,7 @@ impl RefundDbExt for Refund { let meta = api_models::refunds::RefundListMetaData { connector: filter_connector, currency: filter_currency, - status: filter_status, + refund_status: filter_status, }; Ok(meta) @@ -179,12 +196,28 @@ impl RefundDbExt for Refund { .filter(dsl::merchant_id.eq(merchant_id.to_owned())) .into_boxed(); - if let Some(pay_id) = &refund_list_details.payment_id { - filter = filter.filter(dsl::payment_id.eq(pay_id.to_owned())); + let mut search_by_pay_or_ref_id = false; + + if let (Some(pid), Some(ref_id)) = ( + &refund_list_details.payment_id, + &refund_list_details.refund_id, + ) { + search_by_pay_or_ref_id = true; + filter = filter + .filter(dsl::payment_id.eq(pid.to_owned())) + .or_filter(dsl::refund_id.eq(ref_id.to_owned())); + }; + + if !search_by_pay_or_ref_id { + if let Some(pay_id) = &refund_list_details.payment_id { + filter = filter.filter(dsl::payment_id.eq(pay_id.to_owned())); + } } - if let Some(ref_id) = &refund_list_details.refund_id { - filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); + if !search_by_pay_or_ref_id { + if let Some(ref_id) = &refund_list_details.refund_id { + filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); + } } if let Some(profile_id) = &refund_list_details.profile_id { filter = filter.filter(dsl::profile_id.eq(profile_id.to_owned())); diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index f43abdf73ead..41aefc9026d2 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,25 +173,11 @@ 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, @@ -223,6 +212,7 @@ impl ForeignTryFrom for api_enums::RoutableConnectors { api_enums::Connector::Payme => Self::Payme, api_enums::Connector::Paypal => Self::Paypal, api_enums::Connector::Payu => Self::Payu, + api_models::enums::Connector::Placetopay => Self::Placetopay, api_enums::Connector::Plaid => { Err(common_utils::errors::ValidationError::InvalidValue { message: "plaid is not a routable connector".to_string(), @@ -239,6 +229,12 @@ impl ForeignTryFrom for api_enums::RoutableConnectors { }) .into_report()? } + api_enums::Connector::Riskified => { + Err(common_utils::errors::ValidationError::InvalidValue { + message: "riskified is not a routable connector".to_string(), + }) + .into_report()? + } api_enums::Connector::Square => Self::Square, api_enums::Connector::Stax => Self::Stax, api_enums::Connector::Stripe => Self::Stripe, @@ -249,76 +245,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::Bambora => Self::Bambora, - dsl_enums::Connector::Bankofamerica => Self::Bankofamerica, - dsl_enums::Connector::Bitpay => Self::Bitpay, - 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::Prophetpay => Self::Prophetpay, - 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, + }) } } @@ -382,6 +323,7 @@ impl ForeignFrom for data_models::mandates::M data_models::mandates::MandateDataType::MultiUse(None) } }), + update_mandate_id: d.update_mandate_id, } } } @@ -411,10 +353,15 @@ impl ForeignFrom for Option { Some(storage_enums::EventType::ActionRequired) } api_enums::IntentStatus::Cancelled => Some(storage_enums::EventType::PaymentCancelled), + api_enums::IntentStatus::PartiallyCaptured + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => { + Some(storage_enums::EventType::PaymentCaptured) + } + api_enums::IntentStatus::RequiresCapture => { + Some(storage_enums::EventType::PaymentAuthorized) + } api_enums::IntentStatus::RequiresPaymentMethod - | api_enums::IntentStatus::RequiresConfirmation - | api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured => None, + | api_enums::IntentStatus::RequiresConfirmation => None, } } } @@ -517,7 +464,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), @@ -749,6 +697,19 @@ impl ForeignFrom for api_models::disputes::DisputeResponse { } } +impl ForeignFrom for payments::IncrementalAuthorizationResponse { + fn foreign_from(authorization: storage::Authorization) -> Self { + Self { + authorization_id: authorization.authorization_id, + amount: authorization.amount, + status: authorization.status, + error_code: authorization.error_code, + error_message: authorization.error_message, + previously_authorized_amount: authorization.previously_authorized_amount, + } + } +} + impl ForeignFrom for api_models::disputes::DisputeResponsePaymentsRetrieve { fn foreign_from(dispute: storage::Dispute) -> Self { Self { @@ -811,7 +772,7 @@ impl TryFrom for api_models::admin::MerchantCo .parse_value("FrmConfigs") .change_context(errors::ApiErrorResponse::InvalidDataFormat { field_name: "frm_configs".to_string(), - expected_format: "[{ \"gateway\": \"stripe\", \"payment_methods\": [{ \"payment_method\": \"card\",\"payment_method_types\": [{\"payment_method_type\": \"credit\",\"card_networks\": [\"Visa\"],\"flow\": \"pre\",\"action\": \"cancel_txn\"}]}]}]".to_string(), + expected_format: r#"[{ "gateway": "stripe", "payment_methods": [{ "payment_method": "card","payment_method_types": [{"payment_method_type": "credit","card_networks": ["Visa"],"flow": "pre","action": "cancel_txn"}]}]}]"#.to_string(), }) }) .collect::, _>>()?; @@ -847,6 +808,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, }) } } @@ -872,6 +834,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, } } } @@ -984,18 +948,28 @@ impl } } -impl ForeignFrom for api_models::payments::RetrievePaymentLinkResponse { - fn foreign_from(payment_link_object: storage::PaymentLink) -> Self { +impl + ForeignFrom<( + storage::PaymentLink, + api_models::payments::PaymentLinkStatus, + )> for api_models::payments::RetrievePaymentLinkResponse +{ + fn foreign_from( + (payment_link_config, status): ( + storage::PaymentLink, + api_models::payments::PaymentLinkStatus, + ), + ) -> 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, + payment_link_id: payment_link_config.payment_link_id, + merchant_id: payment_link_config.merchant_id, + link_to_pay: payment_link_config.link_to_pay, + amount: payment_link_config.amount, + created_at: payment_link_config.created_at, + expiry: payment_link_config.fulfilment_time, + description: payment_link_config.description, + currency: payment_link_config.currency, + status, } } } @@ -1049,6 +1023,8 @@ 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, } } } @@ -1065,6 +1041,8 @@ impl ForeignFrom for gsm_api_types::GsmResponse { 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 83586e51d66a..68909d1127c5 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -1,11 +1,17 @@ +#[cfg(feature = "olap")] +pub mod connector_onboarding; +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; @@ -45,7 +51,7 @@ use crate::{ types::{encrypt_optional, AsyncLift}, }, storage, - transformers::{ForeignTryFrom, ForeignTryInto}, + transformers::ForeignFrom, }, }; @@ -190,16 +196,6 @@ impl QrImage { } } -#[cfg(test)] -mod tests { - use crate::utils; - #[test] - fn test_image_data_source_url() { - let qr_image_data_source_url = utils::QrImage::new_from_data("Hyperswitch".to_string()); - assert!(qr_image_data_source_url.is_ok()); - } -} - pub async fn find_payment_intent_from_payment_id_type( db: &dyn StorageInterface, payment_id_type: payments::PaymentIdType, @@ -405,6 +401,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, }) } } @@ -684,23 +681,6 @@ pub fn add_apple_pay_payment_status_metrics( } } -impl ForeignTryFrom for enums::EventType { - type Error = errors::ValidationError; - - fn foreign_try_from(value: enums::IntentStatus) -> Result { - match value { - enums::IntentStatus::Succeeded => Ok(Self::PaymentSucceeded), - enums::IntentStatus::Failed => Ok(Self::PaymentFailed), - enums::IntentStatus::Processing => Ok(Self::PaymentProcessing), - enums::IntentStatus::RequiresMerchantAction - | enums::IntentStatus::RequiresCustomerAction => Ok(Self::ActionRequired), - _ => Err(errors::ValidationError::IncorrectValueProvided { - field_name: "intent_status", - }), - } - } -} - pub async fn trigger_payments_webhook( merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, @@ -729,7 +709,9 @@ where if matches!( status, - enums::IntentStatus::Succeeded | enums::IntentStatus::Failed + enums::IntentStatus::Succeeded + | enums::IntentStatus::Failed + | enums::IntentStatus::PartiallyCaptured ) { let payments_response = crate::core::payments::transformers::payments_to_payments_response( req, @@ -745,11 +727,7 @@ where None, )?; - let event_type: enums::EventType = status - .foreign_try_into() - .into_report() - .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) - .attach_printable("payment event type mapping failed")?; + let event_type = ForeignFrom::foreign_from(status); if let services::ApplicationResponse::JsonWithHeaders((payments_response_json, _)) = payments_response @@ -758,27 +736,34 @@ where // 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, + + if let Some(event_type) = event_type { + tokio::spawn( + async move { + Box::pin( + webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook( + m_state, + merchant_account, + business_profile, + event_type, + diesel_models::enums::EventClass::Payments, + None, + payment_id, + diesel_models::enums::EventObjectType::PaymentDetails, + webhooks::OutgoingWebhookContent::PaymentDetails( + payments_response_json, + ), ), - ), - ) - .await - } - .in_current_span(), - ); + ) + .await + } + .in_current_span(), + ); + } else { + logger::warn!( + "Outgoing webhook not sent because of missing event type status mapping" + ); + } } } @@ -797,3 +782,13 @@ pub async fn flatten_join_error(handle: Handle) -> RouterResult { .attach_printable("Join Error"), } } + +#[cfg(test)] +mod tests { + use crate::utils; + #[test] + fn test_image_data_source_url() { + let qr_image_data_source_url = utils::QrImage::new_from_data("Hyperswitch".to_string()); + assert!(qr_image_data_source_url.is_ok()); + } +} diff --git a/crates/router/src/utils/connector_onboarding.rs b/crates/router/src/utils/connector_onboarding.rs new file mode 100644 index 000000000000..03735e61cc70 --- /dev/null +++ b/crates/router/src/utils/connector_onboarding.rs @@ -0,0 +1,143 @@ +use diesel_models::{ConfigNew, ConfigUpdate}; +use error_stack::ResultExt; + +use super::errors::StorageErrorExt; +use crate::{ + consts, + core::errors::{api_error_response::NotImplementedMessage, ApiErrorResponse, RouterResult}, + routes::{app::settings, AppState}, + types::{self, api::enums}, +}; + +pub mod paypal; + +pub fn get_connector_auth( + connector: enums::Connector, + connector_data: &settings::ConnectorOnboarding, +) -> RouterResult { + match connector { + enums::Connector::Paypal => Ok(types::ConnectorAuthType::BodyKey { + api_key: connector_data.paypal.client_secret.clone(), + key1: connector_data.paypal.client_id.clone(), + }), + _ => Err(ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason(format!( + "Onboarding is not implemented for {}", + connector + )), + } + .into()), + } +} + +pub fn is_enabled( + connector: types::Connector, + conf: &settings::ConnectorOnboarding, +) -> Option { + match connector { + enums::Connector::Paypal => Some(conf.paypal.enabled), + _ => None, + } +} + +pub async fn check_if_connector_exists( + state: &AppState, + connector_id: &str, + merchant_id: &str, +) -> RouterResult<()> { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(ApiErrorResponse::MerchantAccountNotFound)?; + + let _connector = state + .store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_id, + connector_id, + &key_store, + ) + .await + .to_not_found_response(ApiErrorResponse::MerchantConnectorAccountNotFound { + id: connector_id.to_string(), + })?; + + Ok(()) +} + +pub async fn set_tracking_id_in_configs( + state: &AppState, + connector_id: &str, + connector: enums::Connector, +) -> RouterResult<()> { + let timestamp = common_utils::date_time::now_unix_timestamp().to_string(); + let find_config = state + .store + .find_config_by_key(&build_key(connector_id, connector)) + .await; + + if find_config.is_ok() { + state + .store + .update_config_by_key( + &build_key(connector_id, connector), + ConfigUpdate::Update { + config: Some(timestamp), + }, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error updating data in configs table")?; + } else if find_config + .as_ref() + .map_err(|e| e.current_context().is_db_not_found()) + .err() + .unwrap_or(false) + { + state + .store + .insert_config(ConfigNew { + key: build_key(connector_id, connector), + config: timestamp, + }) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error inserting data in configs table")?; + } else { + find_config.change_context(ApiErrorResponse::InternalServerError)?; + } + + Ok(()) +} + +pub async fn get_tracking_id_from_configs( + state: &AppState, + connector_id: &str, + connector: enums::Connector, +) -> RouterResult { + let timestamp = state + .store + .find_config_by_key_unwrap_or( + &build_key(connector_id, connector), + Some(common_utils::date_time::now_unix_timestamp().to_string()), + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error getting data from configs table")? + .config; + + Ok(format!("{}_{}", connector_id, timestamp)) +} + +fn build_key(connector_id: &str, connector: enums::Connector) -> String { + format!( + "{}_{}_{}", + consts::CONNECTOR_ONBOARDING_CONFIG_PREFIX, + connector, + connector_id, + ) +} diff --git a/crates/router/src/utils/connector_onboarding/paypal.rs b/crates/router/src/utils/connector_onboarding/paypal.rs new file mode 100644 index 000000000000..6d7f2692be72 --- /dev/null +++ b/crates/router/src/utils/connector_onboarding/paypal.rs @@ -0,0 +1,78 @@ +use common_utils::request::{Method, Request, RequestBuilder, RequestContent}; +use error_stack::{IntoReport, ResultExt}; +use http::header; + +use crate::{ + connector, + core::errors::{ApiErrorResponse, RouterResult}, + routes::AppState, + types, + types::api::{ + enums, + verify_connector::{self as verify_connector_types, VerifyConnector}, + }, + utils::verify_connector as verify_connector_utils, +}; + +pub async fn generate_access_token(state: AppState) -> RouterResult { + let connector = enums::Connector::Paypal; + let boxed_connector = types::api::ConnectorData::convert_connector( + &state.conf.connectors, + connector.to_string().as_str(), + )?; + let connector_auth = super::get_connector_auth(connector, &state.conf.connector_onboarding)?; + + connector::Paypal::get_access_token( + &state, + verify_connector_types::VerifyConnectorData { + connector: *boxed_connector, + connector_auth, + card_details: verify_connector_utils::get_test_card_details(connector)? + .ok_or(ApiErrorResponse::FlowNotSupported { + flow: "Connector onboarding".to_string(), + connector: connector.to_string(), + }) + .into_report()?, + }, + ) + .await? + .ok_or(ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Error occurred while retrieving access token") +} + +pub fn build_paypal_post_request( + url: String, + body: T, + access_token: String, +) -> RouterResult +where + T: serde::Serialize + Send + 'static, +{ + Ok(RequestBuilder::new() + .method(Method::Post) + .url(&url) + .attach_default_headers() + .header( + header::AUTHORIZATION.to_string().as_str(), + format!("Bearer {}", access_token).as_str(), + ) + .header( + header::CONTENT_TYPE.to_string().as_str(), + "application/json", + ) + .set_body(RequestContent::Json(Box::new(body))) + .build()) +} + +pub fn build_paypal_get_request(url: String, access_token: String) -> RouterResult { + Ok(RequestBuilder::new() + .method(Method::Get) + .url(&url) + .attach_default_headers() + .header( + header::AUTHORIZATION.to_string().as_str(), + format!("Bearer {}", access_token).as_str(), + ) + .build()) +} diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs new file mode 100644 index 000000000000..a01f2520b6ac --- /dev/null +++ b/crates/router/src/utils/currency.rs @@ -0,0 +1,728 @@ +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 = "hashicorp-vault")] +use external_services::hashicorp_vault::{self, decrypt::VaultFetch}; +#[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, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, +) -> 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, + #[cfg(feature = "hashicorp-vault")] + hc_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, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, +) -> 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, + #[cfg(feature = "hashicorp-vault")] + hc_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, + #[cfg(feature = "hashicorp-vault")] + hc_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, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, +) -> CustomResult { + match retrieve_forex_from_redis(state).await { + Ok(Some(data)) => { + fallback_forex_redis_check( + state, + data, + call_delay, + #[cfg(feature = "kms")] + kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_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, + #[cfg(feature = "hashicorp-vault")] + hc_config, + ) + .await?) + } + Err(err) => { + logger::error!(?err); + Ok(successive_fetch_and_save_forex( + state, + None, + #[cfg(feature = "kms")] + kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, + ) + .await?) + } + } +} + +async fn successive_fetch_and_save_forex( + state: &AppState, + stale_redis_data: Option, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, +) -> 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, + #[cfg(feature = "hashicorp-vault")] + hc_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, + #[cfg(feature = "hashicorp-vault")] + hc_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, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, +) -> 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, + #[cfg(feature = "hashicorp-vault")] + hc_config, + ) + .await + } + } +} + +async fn handler_local_expired( + state: &AppState, + call_delay: i64, + local_rates: FxExchangeRatesCacheEntry, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, +) -> 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, + #[cfg(feature = "hashicorp-vault")] + hc_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, + #[cfg(feature = "hashicorp-vault")] + hc_config, + ) + .await + } + } +} + +async fn fetch_forex_rates( + state: &AppState, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, +) -> Result> { + let forex_api_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = hashicorp_vault::get_hashicorp_client(hc_config) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.forex_api.api_key.clone(); + #[cfg(feature = "hashicorp-vault")] + let output = state + .conf + .forex_api + .api_key + .clone() + .fetch_inner::(client) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + Ok::<_, error_stack::Report>(output) + } + .await?; + #[cfg(feature = "kms")] + let forex_api_key = kms::get_kms_client(kms_config) + .await + .decrypt(forex_api_key.peek()) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "kms"))] + let forex_api_key = forex_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, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, +) -> CustomResult { + let fallback_api_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = hashicorp_vault::get_hashicorp_client(hc_config) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.forex_api.fallback_api_key.clone(); + #[cfg(feature = "hashicorp-vault")] + let output = state + .conf + .forex_api + .fallback_api_key + .clone() + .fetch_inner::(client) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + Ok::<_, error_stack::Report>(output) + } + .await?; + #[cfg(feature = "kms")] + let fallback_forex_api_key = kms::get_kms_client(kms_config) + .await + .decrypt(fallback_api_key.peek()) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "kms"))] + let fallback_forex_api_key = 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, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, +) -> 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, + #[cfg(feature = "hashicorp-vault")] + hc_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/db_utils.rs b/crates/router/src/utils/db_utils.rs index febc226c0202..219b6f9777f9 100644 --- a/crates/router/src/utils/db_utils.rs +++ b/crates/router/src/utils/db_utils.rs @@ -1,4 +1,7 @@ -use crate::{core::errors, routes::metrics}; +use crate::{ + core::errors::{self, utils::RedisErrorExt}, + routes::metrics, +}; /// Generates hscan field pattern. Suppose the field is pa_1234_ref_1211 it will generate /// pa_1234_ref_* @@ -28,7 +31,8 @@ where metrics::KV_MISS.add(&metrics::CONTEXT, 1, &[]); database_call_closure().await } - _ => Err(redis_error.change_context(errors::StorageError::KVError)), + // Keeping the key empty here since the error would never go here. + _ => Err(redis_error.to_redis_failed_response("")), }, } } diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index c72e4b9feb3c..697d10f772e7 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1 +1,148 @@ +use std::collections::HashMap; + +use api_models::user as user_api; +use diesel_models::{enums::UserStatus, user_role::UserRole}; +use error_stack::ResultExt; +use masking::Secret; + +use crate::{ + core::errors::{UserErrors, UserResult}, + routes::AppState, + services::authentication::{AuthToken, UserFromToken}, + types::domain::{MerchantAccount, UserFromStorage}, +}; + +pub mod dashboard_metadata; pub mod password; +#[cfg(feature = "dummy_connector")] +pub mod sample_data; + +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) + } +} + +pub async fn generate_jwt_auth_token( + state: &AppState, + user: &UserFromStorage, + user_role: &UserRole, +) -> UserResult> { + let token = AuthToken::new_token( + user.get_user_id().to_string(), + user_role.merchant_id.clone(), + user_role.role_id.clone(), + &state.conf, + user_role.org_id.clone(), + ) + .await?; + Ok(Secret::new(token)) +} + +pub async fn generate_jwt_auth_token_with_custom_role_attributes( + state: AppState, + user: &UserFromStorage, + merchant_id: String, + org_id: String, + role_id: String, +) -> UserResult> { + let token = AuthToken::new_token( + user.get_user_id().to_string(), + merchant_id, + role_id, + &state.conf, + org_id, + ) + .await?; + Ok(Secret::new(token)) +} + +pub fn get_dashboard_entry_response( + state: &AppState, + user: UserFromStorage, + user_role: UserRole, + token: Secret, +) -> UserResult { + let verification_days_left = get_verification_days_left(state, &user)?; + + Ok(user_api::DashboardEntryResponse { + merchant_id: user_role.merchant_id, + token, + name: user.get_name(), + email: user.get_email(), + user_id: user.get_user_id().to_string(), + verification_days_left, + user_role: user_role.role_id, + }) +} + +#[allow(unused_variables)] +pub fn get_verification_days_left( + state: &AppState, + user: &UserFromStorage, +) -> UserResult> { + #[cfg(feature = "email")] + return user.get_verification_days_left(state); + #[cfg(not(feature = "email"))] + return Ok(None); +} + +pub fn get_multiple_merchant_details_with_status( + user_roles: Vec, + merchant_accounts: Vec, +) -> UserResult> { + let roles: HashMap<_, _> = user_roles + .into_iter() + .map(|user_role| (user_role.merchant_id.clone(), user_role)) + .collect(); + + merchant_accounts + .into_iter() + .map(|merchant| { + let role = roles + .get(merchant.merchant_id.as_str()) + .ok_or(UserErrors::InternalServerError.into()) + .attach_printable("Merchant exists but user role doesn't")?; + + Ok(user_api::UserMerchantAccount { + merchant_id: merchant.merchant_id.clone(), + merchant_name: merchant.merchant_name.clone(), + is_active: role.status == UserStatus::Active, + }) + }) + .collect() +} 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..bcf270010ea7 --- /dev/null +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -0,0 +1,285 @@ +use std::{net::IpAddr, str::FromStr}; + +use actix_web::http::header::HeaderMap; +use api_models::user::dashboard_metadata::{ + GetMetaDataRequest, GetMultipleMetaDataPayload, ProdIntent, SetMetaDataRequest, +}; +use diesel_models::{ + enums::DashboardMetadata as DBEnum, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew, DashboardMetadataUpdate}, +}; +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 insert_user_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: Some(user_id.clone()), + 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> { + state + .store + .find_merchant_scoped_dashboard_metadata(&merchant_id, &org_id, metadata_keys) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData") +} +pub async fn get_user_scoped_metadata_from_db( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_keys: Vec, +) -> UserResult> { + match state + .store + .find_user_scoped_dashboard_metadata(&user_id, &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 async fn update_merchant_scoped_metadata( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + 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 + .update_metadata( + None, + merchant_id, + org_id, + metadata_key, + DashboardMetadataUpdate::UpdateData { + data_key: metadata_key, + data_value, + last_modified_by: user_id, + }, + ) + .await + .change_context(UserErrors::InternalServerError) +} +pub async fn update_user_scoped_metadata( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + 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 + .update_metadata( + Some(user_id.clone()), + merchant_id, + org_id, + metadata_key, + DashboardMetadataUpdate::UpdateData { + data_key: metadata_key, + data_value, + last_modified_by: user_id, + }, + ) + .await + .change_context(UserErrors::InternalServerError) +} + +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, mut 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::ConfigurationType + | DBEnum::IntegrationCompleted + | DBEnum::StripeConnected + | DBEnum::PaypalConnected + | DBEnum::SpRoutingConfigured + | DBEnum::SpTestPayment + | DBEnum::DownloadWoocom + | DBEnum::ConfigureWoocom + | DBEnum::SetupWoocomWebhook + | DBEnum::IsMultipleConfiguration => merchant_scoped.push(key), + DBEnum::Feedback | DBEnum::ProdIntent => user_scoped.push(key), + } + } + (merchant_scoped, user_scoped) +} + +pub fn is_update_required(metadata: &UserResult) -> bool { + match metadata { + Ok(_) => false, + Err(e) => matches!(e.current_context(), UserErrors::MetadataAlreadySet), + } +} + +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")?, + }) +} + +pub fn is_prod_email_required(data: &ProdIntent) -> bool { + !(data + .poc_email + .as_ref() + .map_or(true, |mail| mail.contains("juspay"))) +} diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs new file mode 100644 index 000000000000..3fa2a10629e5 --- /dev/null +++ b/crates/router/src/utils/user/sample_data.rs @@ -0,0 +1,313 @@ +use api_models::{ + enums::Connector::{DummyConnector4, DummyConnector7}, + user::sample_data::SampleDataRequest, +}; +use data_models::payments::payment_intent::PaymentIntentNew; +use diesel_models::{user::sample_data::PaymentAttemptBatchNew, RefundNew}; +use error_stack::{IntoReport, ResultExt}; +use rand::{prelude::SliceRandom, thread_rng, Rng}; +use time::OffsetDateTime; + +use crate::{ + consts, + core::errors::sample_data::{SampleDataError, SampleDataResult}, + AppState, +}; + +#[allow(clippy::type_complexity)] +pub async fn generate_sample_data( + state: &AppState, + req: SampleDataRequest, + merchant_id: &str, +) -> SampleDataResult)>> { + let merchant_id = merchant_id.to_string(); + let sample_data_size: usize = req.record.unwrap_or(100); + + if !(10..=100).contains(&sample_data_size) { + return Err(SampleDataError::InvalidRange.into()); + } + + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + merchant_id.as_str(), + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(SampleDataError::InternalServerError)?; + + let merchant_from_db = state + .store + .find_merchant_account_by_merchant_id(merchant_id.as_str(), &key_store) + .await + .change_context::(SampleDataError::DataDoesNotExist)?; + + let merchant_parsed_details: Vec = + serde_json::from_value(merchant_from_db.primary_business_details.clone()) + .into_report() + .change_context(SampleDataError::InternalServerError) + .attach_printable("Error while parsing primary business details")?; + + let business_country_default = merchant_parsed_details.first().map(|x| x.country); + + let business_label_default = merchant_parsed_details.first().map(|x| x.business.clone()); + + let profile_id = match crate::core::utils::get_profile_id_from_business_details( + business_country_default, + business_label_default.as_ref(), + &merchant_from_db, + req.profile_id.as_ref(), + &*state.store, + false, + ) + .await + { + Ok(id) => id.clone(), + Err(error) => { + router_env::logger::error!( + "Profile ID not found in business details. Attempting to fetch from the database {error:?}" + ); + + state + .store + .list_business_profile_by_merchant_id(&merchant_id) + .await + .change_context(SampleDataError::InternalServerError) + .attach_printable("Failed to get business profile")? + .first() + .ok_or(SampleDataError::InternalServerError)? + .profile_id + .clone() + } + }; + + // 10 percent payments should be failed + #[allow(clippy::as_conversions)] + let failure_attempts = usize::try_from((sample_data_size as f32 / 10.0).round() as i64) + .into_report() + .change_context(SampleDataError::InvalidParameters)?; + + let failure_after_attempts = sample_data_size / failure_attempts; + + // 20 percent refunds for payments + #[allow(clippy::as_conversions)] + let number_of_refunds = usize::try_from((sample_data_size as f32 / 5.0).round() as i64) + .into_report() + .change_context(SampleDataError::InvalidParameters)?; + + let mut refunds_count = 0; + + let mut random_array: Vec = (1..=sample_data_size).collect(); + + // Shuffle the array + let mut rng = thread_rng(); + random_array.shuffle(&mut rng); + + let mut res: Vec<(PaymentIntentNew, PaymentAttemptBatchNew, Option)> = Vec::new(); + let start_time = req + .start_time + .unwrap_or(common_utils::date_time::now() - time::Duration::days(7)) + .assume_utc() + .unix_timestamp(); + let end_time = req + .end_time + .unwrap_or_else(common_utils::date_time::now) + .assume_utc() + .unix_timestamp(); + + let current_time = common_utils::date_time::now().assume_utc().unix_timestamp(); + + let min_amount = req.min_amount.unwrap_or(100); + let max_amount = req.max_amount.unwrap_or(min_amount + 100); + + if min_amount > max_amount + || start_time > end_time + || start_time > current_time + || end_time > current_time + { + return Err(SampleDataError::InvalidParameters.into()); + }; + + let currency_vec = req.currency.unwrap_or(vec![common_enums::Currency::USD]); + let currency_vec_len = currency_vec.len(); + + let connector_vec = req + .connector + .unwrap_or(vec![DummyConnector4, DummyConnector7]); + let connector_vec_len = connector_vec.len(); + + let auth_type = req.auth_type.unwrap_or(vec![ + common_enums::AuthenticationType::ThreeDs, + common_enums::AuthenticationType::NoThreeDs, + ]); + let auth_type_len = auth_type.len(); + + if currency_vec_len == 0 || connector_vec_len == 0 || auth_type_len == 0 { + return Err(SampleDataError::InvalidParameters.into()); + } + + for num in 1..=sample_data_size { + let payment_id = common_utils::generate_id_with_default_len("test"); + let attempt_id = crate::utils::get_payment_attempt_id(&payment_id, 1); + let client_secret = common_utils::generate_id( + consts::ID_LENGTH, + format!("{}_secret", payment_id.clone()).as_str(), + ); + let amount = thread_rng().gen_range(min_amount..=max_amount); + + let created_at @ modified_at @ last_synced = + OffsetDateTime::from_unix_timestamp(thread_rng().gen_range(start_time..=end_time)) + .map(common_utils::date_time::convert_to_pdt) + .unwrap_or( + req.start_time.unwrap_or_else(|| { + common_utils::date_time::now() - time::Duration::days(7) + }), + ); + let session_expiry = + created_at.saturating_add(time::Duration::seconds(consts::DEFAULT_SESSION_EXPIRY)); + + // After some set of payments sample data will have a failed attempt + let is_failed_payment = + (random_array.get(num - 1).unwrap_or(&0) % failure_after_attempts) == 0; + + let payment_intent = PaymentIntentNew { + payment_id: payment_id.clone(), + merchant_id: merchant_id.clone(), + status: match is_failed_payment { + true => common_enums::IntentStatus::Failed, + _ => common_enums::IntentStatus::Succeeded, + }, + amount: amount * 100, + currency: Some( + *currency_vec + .get((num - 1) % currency_vec_len) + .unwrap_or(&common_enums::Currency::USD), + ), + description: Some("This is a sample payment".to_string()), + created_at: Some(created_at), + modified_at: Some(modified_at), + last_synced: Some(last_synced), + client_secret: Some(client_secret), + business_country: business_country_default, + business_label: business_label_default.clone(), + active_attempt: data_models::RemoteStorageObject::ForeignID(attempt_id.clone()), + attempt_count: 1, + customer_id: Some("hs-dashboard-user".to_string()), + amount_captured: Some(amount * 100), + profile_id: Some(profile_id.clone()), + return_url: Default::default(), + metadata: Default::default(), + connector_id: Default::default(), + shipping_address_id: Default::default(), + billing_address_id: Default::default(), + statement_descriptor_name: Default::default(), + statement_descriptor_suffix: Default::default(), + setup_future_usage: Default::default(), + off_session: Default::default(), + order_details: Default::default(), + allowed_payment_method_types: Default::default(), + connector_metadata: Default::default(), + feature_metadata: Default::default(), + merchant_decision: Default::default(), + payment_link_id: Default::default(), + payment_confirm_source: Default::default(), + updated_by: merchant_from_db.storage_scheme.to_string(), + surcharge_applicable: Default::default(), + request_incremental_authorization: Default::default(), + incremental_authorization_allowed: Default::default(), + authorization_count: Default::default(), + fingerprint_id: None, + session_expiry: Some(session_expiry), + }; + let payment_attempt = PaymentAttemptBatchNew { + attempt_id: attempt_id.clone(), + payment_id: payment_id.clone(), + connector_transaction_id: Some(attempt_id.clone()), + merchant_id: merchant_id.clone(), + status: match is_failed_payment { + true => common_enums::AttemptStatus::Failure, + _ => common_enums::AttemptStatus::Charged, + }, + amount: amount * 100, + currency: payment_intent.currency, + connector: Some( + (*connector_vec + .get((num - 1) % connector_vec_len) + .unwrap_or(&DummyConnector4)) + .to_string(), + ), + payment_method: Some(common_enums::PaymentMethod::Card), + payment_method_type: Some(get_payment_method_type(thread_rng().gen_range(1..=2))), + authentication_type: Some( + *auth_type + .get((num - 1) % auth_type_len) + .unwrap_or(&common_enums::AuthenticationType::NoThreeDs), + ), + error_message: match is_failed_payment { + true => Some("This is a test payment which has a failed status".to_string()), + _ => None, + }, + error_code: match is_failed_payment { + true => Some("HS001".to_string()), + _ => None, + }, + confirm: true, + created_at: Some(created_at), + modified_at: Some(modified_at), + last_synced: Some(last_synced), + amount_to_capture: Some(amount * 100), + connector_response_reference_id: Some(attempt_id.clone()), + updated_by: merchant_from_db.storage_scheme.to_string(), + + ..Default::default() + }; + + let refund = if refunds_count < number_of_refunds && !is_failed_payment { + refunds_count += 1; + Some(RefundNew { + refund_id: common_utils::generate_id_with_default_len("test"), + internal_reference_id: common_utils::generate_id_with_default_len("test"), + external_reference_id: None, + payment_id: payment_id.clone(), + attempt_id: attempt_id.clone(), + merchant_id: merchant_id.clone(), + connector_transaction_id: attempt_id.clone(), + connector_refund_id: None, + description: Some("This is a sample refund".to_string()), + created_at: Some(created_at), + modified_at: Some(modified_at), + refund_reason: Some("Sample Refund".to_string()), + connector: payment_attempt + .connector + .clone() + .unwrap_or(DummyConnector4.to_string()), + currency: *currency_vec + .get((num - 1) % currency_vec_len) + .unwrap_or(&common_enums::Currency::USD), + total_amount: amount * 100, + refund_amount: amount * 100, + refund_status: common_enums::RefundStatus::Success, + sent_to_gateway: true, + refund_type: diesel_models::enums::RefundType::InstantRefund, + metadata: None, + refund_arn: None, + profile_id: payment_intent.profile_id.clone(), + updated_by: merchant_from_db.storage_scheme.to_string(), + merchant_connector_id: payment_attempt.merchant_connector_id.clone(), + }) + } else { + None + }; + + res.push((payment_intent, payment_attempt, refund)); + } + Ok(res) +} + +fn get_payment_method_type(num: u8) -> common_enums::PaymentMethodType { + let rem: u8 = (num) % 2; + match rem { + 0 => common_enums::PaymentMethodType::Debit, + _ => common_enums::PaymentMethodType::Credit, + } +} diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs new file mode 100644 index 000000000000..7ca06aeda0d1 --- /dev/null +++ b/crates/router/src/utils/user_role.rs @@ -0,0 +1,90 @@ +use api_models::user_role as user_role_api; +use diesel_models::{enums::UserStatus, user_role::UserRole}; +use error_stack::ResultExt; + +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_active_user_roles_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(|ele| ele.status == UserStatus::Active) + .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_name().map(|name| { + ( + role_info + .get_permissions() + .iter() + .map(|&per| per.into()) + .collect::>(), + name, + ) + }) +} + +impl From for user_role_api::Permission { + fn from(value: Permission) -> Self { + match value { + Permission::PaymentRead => Self::PaymentRead, + Permission::PaymentWrite => Self::PaymentWrite, + Permission::RefundRead => Self::RefundRead, + Permission::RefundWrite => Self::RefundWrite, + Permission::ApiKeyRead => Self::ApiKeyRead, + Permission::ApiKeyWrite => Self::ApiKeyWrite, + Permission::MerchantAccountRead => Self::MerchantAccountRead, + Permission::MerchantAccountWrite => Self::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead => Self::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite => Self::MerchantConnectorAccountWrite, + Permission::ForexRead => Self::ForexRead, + Permission::RoutingRead => Self::RoutingRead, + Permission::RoutingWrite => Self::RoutingWrite, + Permission::DisputeRead => Self::DisputeRead, + Permission::DisputeWrite => Self::DisputeWrite, + Permission::MandateRead => Self::MandateRead, + Permission::MandateWrite => Self::MandateWrite, + Permission::CustomerRead => Self::CustomerRead, + Permission::CustomerWrite => Self::CustomerWrite, + Permission::FileRead => Self::FileRead, + Permission::FileWrite => Self::FileWrite, + Permission::Analytics => Self::Analytics, + Permission::ThreeDsDecisionManagerWrite => Self::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead => Self::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite => Self::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead => Self::SurchargeDecisionManagerRead, + Permission::UsersRead => Self::UsersRead, + Permission::UsersWrite => Self::UsersWrite, + Permission::MerchantAccountCreate => Self::MerchantAccountCreate, + } + } +} diff --git a/crates/router/src/utils/verify_connector.rs b/crates/router/src/utils/verify_connector.rs new file mode 100644 index 000000000000..060f92d0e5a8 --- /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: Some(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/api_key_expiry.rs b/crates/router/src/workflows/api_key_expiry.rs index 62d8a54c4024..eb3c1d9c1ce9 100644 --- a/crates/router/src/workflows/api_key_expiry.rs +++ b/crates/router/src/workflows/api_key_expiry.rs @@ -115,6 +115,12 @@ Team Hyperswitch"), let task_ids = vec![task_id]; db.process_tracker_update_process_status_by_ids(task_ids, updated_process_tracker_data) .await?; + // Remaining tasks are re-scheduled, so will be resetting the added count + metrics::TASKS_RESET_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes("flow", "ApiKeyExpiry")], + ); } Ok(()) diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 00e7357d896f..b2296e17f70d 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -124,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, @@ -136,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 @@ -298,7 +301,10 @@ mod tests { let cpt_default = process_data::ConnectorPTMapping::default().default_mapping; assert_eq!( vec![schedule_time_delta, first_retry_time_delta], - vec![cpt_default.start_after, cpt_default.frequency[0]] + vec![ + cpt_default.start_after, + *cpt_default.frequency.first().unwrap() + ] ); } } diff --git a/crates/router/tests/cache.rs b/crates/router/tests/cache.rs index 4de45c7132a8..040e0dddf97c 100644 --- a/crates/router/tests/cache.rs +++ b/crates/router/tests/cache.rs @@ -50,8 +50,8 @@ async fn invalidate_existing_cache_success() { let response_body = response.body().await; println!("invalidate Cache: {response:?} : {response_body:?}"); assert_eq!(response.status(), awc::http::StatusCode::OK); - assert!(cache::CONFIG_CACHE.get(&cache_key).is_none()); - assert!(cache::ACCOUNTS_CACHE.get(&cache_key).is_none()); + assert!(cache::CONFIG_CACHE.get(&cache_key).await.is_none()); + assert!(cache::ACCOUNTS_CACHE.get(&cache_key).await.is_none()); } #[actix_web::test] diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index c9ee3a34f2ef..d3f8147fb262 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -38,7 +38,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { card_number: cards::CardNumber::from_str("4200000000000000").unwrap(), card_exp_month: Secret::new("10".to_string()), card_exp_year: Secret::new("2025".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("999".to_string()), card_issuer: None, card_network: None, @@ -59,6 +59,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { order_details: None, order_category: None, email: None, + customer_name: None, session_token: None, enrolled_for_3ds: false, related_transaction_id: None, @@ -69,6 +70,8 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, + metadata: None, }, response: Err(types::ErrorResponse::default()), payment_method_id: None, @@ -94,6 +97,9 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { connector_http_status_code: None, apple_pay_flow: None, external_latency: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, } } @@ -152,6 +158,9 @@ fn construct_refund_router_data() -> types::RefundsRouterData { connector_http_status_code: None, apple_pay_flow: None, external_latency: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, } } @@ -160,6 +169,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 +214,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, @@ -229,7 +240,7 @@ async fn payments_create_failure() { card_number: cards::CardNumber::from_str("4200000000000000").unwrap(), card_exp_month: Secret::new("10".to_string()), card_exp_year: Secret::new("2025".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("99".to_string()), card_issuer: None, card_network: None, @@ -265,6 +276,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 +345,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..430ae0bac147 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -95,16 +95,16 @@ impl AdyenTest { card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), expiry_month: Secret::new("3".to_string()), expiry_year: Secret::new("2030".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(Secret::new("John Doe".to_string())), })) } enums::PayoutType::Bank => Some(api::PayoutMethodData::Bank( api::payouts::BankPayout::Sepa(api::SepaBankTransfer { iban: "NL46TEST0136169112".to_string().into(), bic: Some("ABNANL2A".to_string().into()), - bank_name: "Deutsche Bank".to_string(), - bank_country_code: enums::CountryAlpha2::NL, - bank_city: "Amsterdam".to_string(), + bank_name: Some("Deutsche Bank".to_string()), + bank_country_code: Some(enums::CountryAlpha2::NL), + bank_city: Some("Amsterdam".to_string()), }), )), }, @@ -126,7 +126,7 @@ impl AdyenTest { card_number: cards::CardNumber::from_str(card_number).unwrap(), card_exp_month: Secret::new(card_exp_month.to_string()), card_exp_year: Secret::new(card_exp_year.to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new(card_cvc.to_string()), card_issuer: None, card_network: None, @@ -147,6 +147,7 @@ impl AdyenTest { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type: None, session_token: None, @@ -157,6 +158,8 @@ impl AdyenTest { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, + metadata: None, }) } } diff --git a/crates/router/tests/connectors/airwallex.rs b/crates/router/tests/connectors/airwallex.rs index 6e7f6c000d28..cfc4c0c003d2 100644 --- a/crates/router/tests/connectors/airwallex.rs +++ b/crates/router/tests/connectors/airwallex.rs @@ -60,7 +60,7 @@ fn payment_method_details() -> Option { card_number: cards::CardNumber::from_str("4035501000000008").unwrap(), card_exp_month: Secret::new("02".to_string()), card_exp_year: Secret::new("2035".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("123".to_string()), card_issuer: None, card_network: None, diff --git a/crates/router/tests/connectors/authorizedotnet.rs b/crates/router/tests/connectors/authorizedotnet.rs index 4021d57d543f..3ae4298e8360 100644 --- a/crates/router/tests/connectors/authorizedotnet.rs +++ b/crates/router/tests/connectors/authorizedotnet.rs @@ -42,7 +42,7 @@ fn get_payment_method_data() -> api::Card { card_number: cards::CardNumber::from_str("5424000000000015").unwrap(), card_exp_month: Secret::new("02".to_string()), card_exp_year: Secret::new("2035".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("123".to_string()), ..Default::default() } diff --git a/crates/router/tests/connectors/bitpay.rs b/crates/router/tests/connectors/bitpay.rs index 755427140c4f..892d5b1f208f 100644 --- a/crates/router/tests/connectors/bitpay.rs +++ b/crates/router/tests/connectors/bitpay.rs @@ -81,6 +81,7 @@ fn payment_method_details() -> Option { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type: None, session_token: None, @@ -92,6 +93,8 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, + metadata: None, }) } diff --git a/crates/router/tests/connectors/bluesnap.rs b/crates/router/tests/connectors/bluesnap.rs index 30052d11da45..852b23f022c3 100644 --- a/crates/router/tests/connectors/bluesnap.rs +++ b/crates/router/tests/connectors/bluesnap.rs @@ -400,7 +400,7 @@ async fn should_fail_payment_for_incorrect_cvc() { Some(types::PaymentsAuthorizeData { email: Some(Email::from_str("test@gmail.com").unwrap()), payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("12345".to_string()), ..utils::CCardType::default().0 }), @@ -426,7 +426,7 @@ async fn should_fail_payment_for_invalid_exp_month() { Some(types::PaymentsAuthorizeData { email: Some(Email::from_str("test@gmail.com").unwrap()), payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_exp_month: Secret::new("20".to_string()), ..utils::CCardType::default().0 }), @@ -452,7 +452,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() { Some(types::PaymentsAuthorizeData { email: Some(Email::from_str("test@gmail.com").unwrap()), payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_exp_year: Secret::new("2000".to_string()), ..utils::CCardType::default().0 }), diff --git a/crates/router/tests/connectors/cashtocode.rs b/crates/router/tests/connectors/cashtocode.rs index 871677bb692a..9d0824457199 100644 --- a/crates/router/tests/connectors/cashtocode.rs +++ b/crates/router/tests/connectors/cashtocode.rs @@ -57,6 +57,7 @@ impl CashtocodeTest { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type, session_token: None, @@ -67,6 +68,8 @@ impl CashtocodeTest { complete_authorize_url: None, customer_id: Some("John Doe".to_owned()), surcharge_details: None, + request_incremental_authorization: false, + metadata: None, }) } diff --git a/crates/router/tests/connectors/coinbase.rs b/crates/router/tests/connectors/coinbase.rs index 512e03a5c94d..9a476df7fe63 100644 --- a/crates/router/tests/connectors/coinbase.rs +++ b/crates/router/tests/connectors/coinbase.rs @@ -83,6 +83,7 @@ fn payment_method_details() -> Option { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type: None, session_token: None, @@ -94,6 +95,8 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, + metadata: None, }) } diff --git a/crates/router/tests/connectors/cryptopay.rs b/crates/router/tests/connectors/cryptopay.rs index e9c43cee3af6..5e1b3f5ab47b 100644 --- a/crates/router/tests/connectors/cryptopay.rs +++ b/crates/router/tests/connectors/cryptopay.rs @@ -81,6 +81,7 @@ fn payment_method_details() -> Option { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type: None, session_token: None, @@ -92,6 +93,8 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, + metadata: None, }) } diff --git a/crates/router/tests/connectors/fiserv.rs b/crates/router/tests/connectors/fiserv.rs index 1394667718c5..36d5f66dbccd 100644 --- a/crates/router/tests/connectors/fiserv.rs +++ b/crates/router/tests/connectors/fiserv.rs @@ -46,7 +46,7 @@ fn payment_method_details() -> Option { card_number: cards::CardNumber::from_str("4005550000000019").unwrap(), card_exp_month: Secret::new("02".to_string()), card_exp_year: Secret::new("2035".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("123".to_string()), card_issuer: None, card_network: None, diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index fc474818b505..8db743d6098e 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -45,6 +45,7 @@ mod payeezy; mod payme; mod paypal; mod payu; +mod placetopay; mod powertranz; #[cfg(feature = "dummy_connector")] mod prophetpay; diff --git a/crates/router/tests/connectors/opennode.rs b/crates/router/tests/connectors/opennode.rs index 248bbb02e520..69edec2af2cf 100644 --- a/crates/router/tests/connectors/opennode.rs +++ b/crates/router/tests/connectors/opennode.rs @@ -82,6 +82,7 @@ fn payment_method_details() -> Option { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type: None, session_token: None, @@ -93,6 +94,8 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, + metadata: None, }) } diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 5550ba12af88..206694be0e8b 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -78,6 +78,11 @@ fn payment_method_details() -> Option { quantity: 1, amount: 1000, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -87,7 +92,7 @@ fn payment_method_details() -> Option { card_cvc: Secret::new("123".to_string()), card_exp_month: Secret::new("10".to_string()), card_exp_year: Secret::new("2025".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), ..utils::CCardType::default().0 }), amount: 1000, @@ -373,6 +378,11 @@ async fn should_fail_payment_for_incorrect_cvc() { quantity: 1, amount: 100, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -406,6 +416,11 @@ async fn should_fail_payment_for_invalid_exp_month() { quantity: 1, amount: 100, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -439,6 +454,11 @@ async fn should_fail_payment_for_incorrect_expiry_year() { quantity: 1, amount: 100, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), diff --git a/crates/router/tests/connectors/placetopay.rs b/crates/router/tests/connectors/placetopay.rs new file mode 100644 index 000000000000..b7c70c789da2 --- /dev/null +++ b/crates/router/tests/connectors/placetopay.rs @@ -0,0 +1,420 @@ +use masking::Secret; +use router::types::{self, api, storage::enums}; +use test_utils::connector_auth; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct PlacetopayTest; +impl ConnectorActions for PlacetopayTest {} +impl utils::Connector for PlacetopayTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Placetopay; + types::api::ConnectorData { + connector: Box::new(&Placetopay), + connector_name: types::Connector::Placetopay, + get_token: types::api::GetToken::Connector, + merchant_connector_id: None, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .placetopay + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "placetopay".to_string() + } +} + +static CONNECTOR: PlacetopayTest = PlacetopayTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenerios +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's security code is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration month is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration year is invalid.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("No such payment_intent: '123456789'") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/crates/router/tests/connectors/rapyd.rs b/crates/router/tests/connectors/rapyd.rs index 143f87fc5753..c53f4e8d8b1f 100644 --- a/crates/router/tests/connectors/rapyd.rs +++ b/crates/router/tests/connectors/rapyd.rs @@ -46,7 +46,7 @@ async fn should_only_authorize_payment() { card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), card_exp_month: Secret::new("02".to_string()), card_exp_year: Secret::new("2024".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("123".to_string()), card_issuer: None, card_network: None, @@ -74,7 +74,7 @@ async fn should_authorize_and_capture_payment() { card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), card_exp_month: Secret::new("02".to_string()), card_exp_year: Secret::new("2024".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("123".to_string()), card_issuer: None, card_network: None, diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index f8f6039d6d36..68cf6f680355 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -108,7 +108,7 @@ api_key = "API Key" [iatapay] key1 = "key1" api_key = "api_key" -api_secret = "secrect" +api_secret = "secret" [dummyconnector] api_key = "API Key" @@ -189,3 +189,7 @@ api_key="API Key" api_key = "MyApiKey" key1 = "Merchant id" api_secret = "Secret key" + +[placetopay] +api_key= "Login" +key1= "Trankey" diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 67a0625968fb..844777e2dcab 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -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, @@ -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, @@ -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, @@ -455,7 +458,7 @@ pub trait ConnectorActions: Connector { customer_details: Some(payments::CustomerDetails { customer_id: core_utils::get_or_generate_id("customer_id", &None, "cust_").ok(), name: Some(Secret::new("John Doe".to_string())), - email: TryFrom::try_from(Secret::new("john.doe@example".to_string())).ok(), + email: Email::from_str("john.doe@example").ok(), phone: Some(Secret::new("620874518".to_string())), phone_country_code: Some("+31".to_string()), }), @@ -520,6 +523,9 @@ pub trait ConnectorActions: Connector { connector_http_status_code: None, apple_pay_flow: None, external_latency: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, } } @@ -539,6 +545,7 @@ pub trait ConnectorActions: Connector { Ok(types::PaymentsResponseData::PreProcessingResponse { .. }) => None, Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None, Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None, + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { .. }) => None, Err(_) => None, } } @@ -561,6 +568,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 +609,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 +651,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 +693,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 +781,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 +814,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, @@ -859,7 +872,7 @@ impl Default for CCardType { card_number: cards::CardNumber::from_str("4200000000000000").unwrap(), card_exp_month: Secret::new("10".to_string()), card_exp_year: Secret::new("2025".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("999".to_string()), card_issuer: None, card_network: None, @@ -889,6 +902,7 @@ impl Default for PaymentAuthorizeType { order_details: None, order_category: None, email: None, + customer_name: None, session_token: None, enrolled_for_3ds: false, related_transaction_id: None, @@ -899,6 +913,8 @@ impl Default for PaymentAuthorizeType { webhook_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, + metadata: None, }; Self(data) } @@ -983,7 +999,7 @@ impl Default for CustomerType { let data = types::ConnectorCustomerData { payment_method_data: types::api::PaymentMethodData::Card(CCardType::default().0), description: None, - email: Some(Email::from(Secret::new("test@juspay.in".to_string()))), + email: Email::from_str("test@juspay.in").ok(), phone: None, name: None, preprocessing_id: None, @@ -1019,6 +1035,7 @@ pub fn get_connector_transaction_id( Ok(types::PaymentsResponseData::ConnectorCustomerResponse { .. }) => None, Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None, Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None, + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { .. }) => None, Err(_) => None, } } @@ -1034,6 +1051,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/wise.rs b/crates/router/tests/connectors/wise.rs index fb65397e1a22..de303523040e 100644 --- a/crates/router/tests/connectors/wise.rs +++ b/crates/router/tests/connectors/wise.rs @@ -73,9 +73,9 @@ impl WiseTest { api::BacsBankTransfer { bank_sort_code: "231470".to_string().into(), bank_account_number: "28821822".to_string().into(), - bank_name: "Deutsche Bank".to_string(), - bank_country_code: enums::CountryAlpha2::NL, - bank_city: "Amsterdam".to_string(), + bank_name: Some("Deutsche Bank".to_string()), + bank_country_code: Some(enums::CountryAlpha2::NL), + bank_city: Some("Amsterdam".to_string()), }, ))), ..Default::default() diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index 6163949c6c58..8b8657890039 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -71,7 +71,7 @@ impl WorldlineTest { card_number: cards::CardNumber::from_str(card_number).unwrap(), card_exp_month: Secret::new(card_exp_month.to_string()), card_exp_year: Secret::new(card_exp_year.to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new(card_cvc.to_string()), card_issuer: None, card_network: None, @@ -92,6 +92,7 @@ impl WorldlineTest { order_details: None, order_category: None, email: None, + customer_name: None, session_token: None, enrolled_for_3ds: false, related_transaction_id: None, @@ -102,6 +103,8 @@ impl WorldlineTest { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, + metadata: None, }) } } diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index 12f914e13c1a..c3bce7d51c39 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -315,6 +315,11 @@ async fn should_fail_payment_for_incorrect_card_number() { quantity: 1, amount: 1000, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -351,6 +356,11 @@ async fn should_fail_payment_for_incorrect_cvc() { quantity: 1, amount: 1000, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -387,6 +397,11 @@ async fn should_fail_payment_for_invalid_exp_month() { quantity: 1, amount: 1000, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -423,6 +438,11 @@ async fn should_fail_payment_for_incorrect_expiry_year() { quantity: 1, amount: 1000, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), diff --git a/crates/router/tests/integration_demo.rs b/crates/router/tests/integration_demo.rs index 5bdf9a5f525e..777e0a682985 100644 --- a/crates/router/tests/integration_demo.rs +++ b/crates/router/tests/integration_demo.rs @@ -153,7 +153,7 @@ async fn exceed_refund() { let message: serde_json::Value = user_client.create_refund(&server, &payment_id, 100).await; assert_eq!( - message["error"]["message"], - "Refund amount exceeds the payment amount." + message.get("error").unwrap().get("message").unwrap(), + "The refund amount exceeds the amount captured." ); } diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index 9d48aaddd451..8f5ac25736cc 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -320,7 +320,7 @@ async fn payments_create_core() { card_number: "4242424242424242".to_string().try_into().unwrap(), card_exp_month: "10".to_string().into(), card_exp_year: "35".to_string().into(), - card_holder_name: "Arun Raj".to_string().into(), + card_holder_name: Some(masking::Secret::new("Arun Raj".to_string())), card_cvc: "123".to_string().into(), card_issuer: None, card_network: None, @@ -496,7 +496,7 @@ async fn payments_create_core_adyen_no_redirect() { card_number: "5555 3412 4444 1115".to_string().try_into().unwrap(), card_exp_month: "03".to_string().into(), card_exp_year: "2030".to_string().into(), - card_holder_name: "JohnDoe".to_string().into(), + card_holder_name: Some(masking::Secret::new("JohnDoe".to_string())), card_cvc: "737".to_string().into(), card_issuer: None, card_network: None, diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index 5d4ca844061f..89ac522d237a 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -80,7 +80,7 @@ async fn payments_create_core() { card_number: "4242424242424242".to_string().try_into().unwrap(), card_exp_month: "10".to_string().into(), card_exp_year: "35".to_string().into(), - card_holder_name: "Arun Raj".to_string().into(), + card_holder_name: Some(masking::Secret::new("Arun Raj".to_string())), card_cvc: "123".to_string().into(), card_issuer: None, card_network: None, @@ -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, @@ -262,7 +263,7 @@ async fn payments_create_core_adyen_no_redirect() { card_number: "5555 3412 4444 1115".to_string().try_into().unwrap(), card_exp_month: "03".to_string().into(), card_exp_year: "2030".to_string().into(), - card_holder_name: "JohnDoe".to_string().into(), + card_holder_name: Some(masking::Secret::new("JohnDoe".to_string())), card_cvc: "737".to_string().into(), bank_code: None, card_issuer: None, diff --git a/crates/router/tests/refunds.rs b/crates/router/tests/refunds.rs index 6b9dfd5ed4a2..8d6a158cdff2 100644 --- a/crates/router/tests/refunds.rs +++ b/crates/router/tests/refunds.rs @@ -19,7 +19,7 @@ async fn refund_create_fail_stripe() { let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let refund: serde_json::Value = user_client.create_refund(&app, &payment_id, 10).await; - assert_eq!(refund["error"]["message"], "Access forbidden, invalid API key was used. Please create your new API key from the Dashboard Settings section."); + assert_eq!(refund.get("error").unwrap().get("message").unwrap(), "Access forbidden, invalid API key was used. Please create your new API key from the Dashboard Settings section."); } #[actix_web::test] @@ -33,7 +33,7 @@ async fn refund_create_fail_adyen() { let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let refund: serde_json::Value = user_client.create_refund(&app, &payment_id, 10).await; - assert_eq!(refund["error"]["message"], "Access forbidden, invalid API key was used. Please create your new API key from the Dashboard Settings section."); + assert_eq!(refund.get("error").unwrap().get("message").unwrap(), "Access forbidden, invalid API key was used. Please create your new API key from the Dashboard Settings section."); } #[actix_web::test] diff --git a/crates/router/tests/utils.rs b/crates/router/tests/utils.rs index 6cddbc043662..339eca6fa0fb 100644 --- a/crates/router/tests/utils.rs +++ b/crates/router/tests/utils.rs @@ -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..07e95b9232c3 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" +indexmap = "2.1.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"] } +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" + diff --git a/crates/router_derive/src/lib.rs b/crates/router_derive/src/lib.rs index 3f34c156ae8f..c4b6bf3f470e 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 @@ -501,7 +505,10 @@ pub fn operation_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre /// } /// ``` -#[proc_macro_derive(PolymorphicSchema, attributes(mandatory_in, generate_schemas))] +#[proc_macro_derive( + PolymorphicSchema, + attributes(mandatory_in, generate_schemas, remove_in) +)] pub fn polymorphic_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); 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..4dd6db5d60df 100644 --- a/crates/router_derive/src/macros/generate_schema.rs +++ b/crates/router_derive/src/macros/generate_schema.rs @@ -1,7 +1,7 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use indexmap::IndexMap; -use syn::{self, parse_quote, punctuated::Punctuated, Token}; +use syn::{self, parse::Parse, parse_quote, punctuated::Punctuated, Token}; use crate::macros::helpers; @@ -19,6 +19,74 @@ fn get_inner_path_ident(attribute: &syn::Attribute) -> syn::Result>()) } +#[allow(dead_code)] +/// Get the type of field +fn get_field_type(field_type: syn::Type) -> syn::Result { + if let syn::Type::Path(path) = field_type { + path.path + .segments + .last() + .map(|last_path_segment| last_path_segment.ident.to_owned()) + .ok_or(syn::Error::new( + proc_macro2::Span::call_site(), + "Atleast one ident must be specified", + )) + } else { + Err(syn::Error::new( + proc_macro2::Span::call_site(), + "Only path fields are supported", + )) + } +} + +#[allow(dead_code)] +/// Get the inner type of option +fn get_inner_option_type(field: &syn::Type) -> syn::Result { + if let syn::Type::Path(ref path) = &field { + if let Some(segment) = path.path.segments.last() { + if let syn::PathArguments::AngleBracketed(ref args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(ty)) = args.args.first() { + return get_field_type(ty.clone()); + } + } + } + } + + Err(syn::Error::new( + proc_macro2::Span::call_site(), + "Only path fields are supported", + )) +} + +mod schema_keyword { + use syn::custom_keyword; + + custom_keyword!(schema); +} + +#[derive(Debug, Clone)] +pub struct SchemaMeta { + struct_name: syn::Ident, + type_ident: syn::Ident, +} + +/// parse #[mandatory_in(PaymentsCreateRequest = u64)] +impl Parse for SchemaMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let struct_name = input.parse::()?; + input.parse::()?; + let type_ident = input.parse::()?; + Ok(Self { + struct_name, + type_ident, + }) + } +} + +impl quote::ToTokens for SchemaMeta { + fn to_tokens(&self, _: &mut proc_macro2::TokenStream) {} +} + pub fn polymorphic_macro_derive_inner( input: syn::DeriveInput, ) -> syn::Result { @@ -30,24 +98,41 @@ pub fn polymorphic_macro_derive_inner( // Go through all the fields and create a mapping of required fields for a schema // PaymentsCreate -> ["amount","currency"] - // This will be stored in a hashset + // This will be stored in the hashmap with key as // required_fields -> ((amount, PaymentsCreate), (currency, PaymentsCreate)) - let mut required_fields = HashSet::<(syn::Ident, syn::Ident)>::new(); + // and values as the type + // + // (amount, PaymentsCreate) -> Amount + let mut required_fields = HashMap::<(syn::Ident, syn::Ident), syn::Ident>::new(); + + // These fields will be removed in the schema + // PaymentsUpdate -> ["client_secret"] + // This will be stored in a hashset + // hide_fields -> ((client_secret, PaymentsUpdate)) + let mut hide_fields = HashSet::<(syn::Ident, syn::Ident)>::new(); let mut all_fields = IndexMap::>::new(); - fields.iter().for_each(|field| { + for field in fields { // Partition the attributes of a field into two vectors // One with #[mandatory_in] attributes present // Rest of the attributes ( include only the schema attribute, serde is not required) let (mandatory_attribute, other_attributes) = field .attrs .iter() - .partition::, _>(|attribute| attribute.path.is_ident("mandatory_in")); + .partition::, _>(|attribute| attribute.path().is_ident("mandatory_in")); + + let hidden_fields = field + .attrs + .iter() + .filter(|attribute| attribute.path().is_ident("remove_in")) + .collect::>(); // 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 @@ -66,7 +151,31 @@ pub fn polymorphic_macro_derive_inner( // ("currency", PaymentsConfirmRequest) // // For these attributes, we need to later add #[schema(required = true)] attribute - _ = mandatory_attribute + let field_ident = field.ident.ok_or(syn::Error::new( + proc_macro2::Span::call_site(), + "Cannot use `mandatory_in` on unnamed fields", + ))?; + + // Parse the #[mandatory_in(PaymentsCreateRequest = u64)] and insert into hashmap + // key -> ("amount", PaymentsCreateRequest) + // value -> u64 + if let Some(mandatory_in_attribute) = + helpers::get_metadata_inner::("mandatory_in", mandatory_attribute)?.first() + { + let key = ( + field_ident.clone(), + mandatory_in_attribute.struct_name.clone(), + ); + let value = mandatory_in_attribute.type_ident.clone(); + required_fields.insert(key, value); + } + + // Hidden fields are to be inserted in the Hashset + // The hashset will store it in this format + // ("client_secret", PaymentsUpdate) + // + // These fields will not be added to the struct + _ = hidden_fields .iter() // Filter only #[mandatory_in] attributes .map(|&attribute| get_inner_path_ident(attribute)) @@ -74,40 +183,59 @@ pub fn polymorphic_macro_derive_inner( let res = schemas .map_err(|error| syn::Error::new(proc_macro2::Span::call_site(), error))? .iter() - .filter_map(|schema| field.ident.to_owned().zip(Some(schema.to_owned()))) + .map(|schema| (field_ident.clone(), schema.to_owned())) .collect::>(); - required_fields.extend(res); + hide_fields.extend(res); Ok::<_, syn::Error>(()) }); - }); + } + // iterate over the schemas and build them with their fields let schemas = schemas_to_create .iter() .map(|schema| { let fields = all_fields .iter() - .flat_map(|(field, value)| { - let mut attributes = value - .iter() - .map(|attribute| quote::quote!(#attribute)) - .collect::>(); - - // If the field is required for this schema, then add - // #[schema(required = true)] for this field - let required_attribute: syn::Attribute = - parse_quote!(#[schema(required = true)]); - - // Can be none, because tuple fields have no ident - field.ident.to_owned().and_then(|field_ident| { - required_fields - .contains(&(field_ident, schema.to_owned())) - .then(|| attributes.push(quote::quote!(#required_attribute))) - }); - - quote::quote! { - #(#attributes)* - #field, + .filter_map(|(field, attributes)| { + let mut final_attributes = attributes.clone(); + + if let Some(field_ident) = field.ident.to_owned() { + // If the field is required for this schema, then add + // #[schema(value_type = type)] for this field + if let Some(required_field_type) = + required_fields.get(&(field_ident.clone(), schema.to_owned())) + { + // This is a required field in the Schema + // Add the value type and remove original value type ( if present ) + let attribute_without_schema_type = attributes + .iter() + .filter(|attribute| !attribute.path().is_ident("schema")) + .map(Clone::clone) + .collect::>(); + + final_attributes = attribute_without_schema_type; + + let value_type_attribute: syn::Attribute = + parse_quote!(#[schema(value_type = #required_field_type)]); + final_attributes.push(value_type_attribute); + } + } + + // If the field is to be not shown then + let is_hidden_field = field + .ident + .clone() + .map(|field_ident| hide_fields.contains(&(field_ident, schema.to_owned()))) + .unwrap_or(false); + + if is_hidden_field { + None + } else { + Some(quote::quote! { + #(#final_attributes)* + #field, + }) } }) .collect::>(); 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..e743a2d9cc52 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -1,55 +1,34 @@ -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, Session, 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, - } - } + IncrementalAuthorization, + IncrementalAuthorizationData, } impl Derives { @@ -82,8 +61,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 +73,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()) } @@ -131,6 +97,12 @@ impl Conversion { } Derives::Session => syn::Ident::new("PaymentsSessionRequest", Span::call_site()), Derives::SessionData => syn::Ident::new("PaymentsSessionData", Span::call_site()), + Derives::IncrementalAuthorization => { + syn::Ident::new("PaymentsIncrementalAuthorizationRequest", Span::call_site()) + } + Derives::IncrementalAuthorizationData => { + syn::Ident::new("PaymentsIncrementalAuthorizationData", Span::call_site()) + } } } @@ -231,103 +203,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; @@ -347,6 +422,7 @@ pub fn operation_derive_inner(input: DeriveInput) -> syn::Result syn::Result) -> 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/Cargo.toml b/crates/router_env/Cargo.toml index 266baf0e3863..579f4ef10e7c 100644 --- a/crates/router_env/Cargo.toml +++ b/crates/router_env/Cargo.toml @@ -16,22 +16,22 @@ once_cell = "1.18.0" opentelemetry = { version = "0.19.0", features = ["rt-tokio-current-thread", "metrics"] } opentelemetry-otlp = { version = "0.12.0", features = ["metrics"] } rustc-hash = "1.1" -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" -serde_path_to_error = "0.1.11" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" +serde_path_to_error = "0.1.14" strum = { version = "0.24.1", features = ["derive"] } time = { version = "0.3.21", default-features = false, features = ["formatting"] } -tokio = { version = "1.28.2" } -tracing = { version = "=0.1.36" } +tokio = { version = "1.35.1" } +tracing = { version = "0.1.37" } tracing-actix-web = { version = "0.7.8", features = ["opentelemetry_0_19", "uuid_v7"], optional = true } tracing-appender = { version = "0.2.2" } -tracing-attributes = "=0.1.22" +tracing-attributes = "0.1.27" tracing-opentelemetry = { version = "0.19.0" } tracing-subscriber = { version = "0.3.17", default-features = true, features = ["env-filter", "json", "registry"] } vergen = { version = "8.2.1", optional = true, features = ["cargo", "git", "git2", "rustc"] } [dev-dependencies] -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } [build-dependencies] cargo_metadata = "0.15.4" diff --git a/crates/router_env/src/env.rs b/crates/router_env/src/env.rs index e57c38e7e4ad..644f9b50a126 100644 --- a/crates/router_env/src/env.rs +++ b/crates/router_env/src/env.rs @@ -167,3 +167,13 @@ macro_rules! profile { env!("CARGO_PROFILE") }; } + +/// The latest git tag. If tags are not present in the repository, the short commit hash is used +/// instead. Refer to the [`git describe`](https://git-scm.com/docs/git-describe) documentation for +/// more details. +#[macro_export] +macro_rules! git_tag { + () => { + env!("VERGEN_GIT_DESCRIBE") + }; +} diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index e75606aa1531..9139b5eed417 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -39,10 +39,21 @@ 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, + GetConnectorEvents, + GetOutgoingWebhookEvents, } impl FlowMetric for AnalyticsFlow {} diff --git a/crates/router_env/src/logger/storage.rs b/crates/router_env/src/logger/storage.rs index 04f00f71cb3e..036a73cf874d 100644 --- a/crates/router_env/src/logger/storage.rs +++ b/crates/router_env/src/logger/storage.rs @@ -77,8 +77,12 @@ impl Visit for Storage<'_> { // Skip fields which are already handled name if name.starts_with("log.") => (), name if name.starts_with("r#") => { - self.values - .insert(&name[2..], serde_json::Value::from(format!("{value:?}"))); + self.values.insert( + #[allow(clippy::expect_used)] + name.get(2..) + .expect("field name must have a minimum of two characters"), + serde_json::Value::from(format!("{value:?}")), + ); } name => { self.values @@ -88,13 +92,16 @@ impl Visit for Storage<'_> { } } -#[allow(clippy::expect_used)] +const PERSISTENT_KEYS: [&str; 4] = ["payment_id", "connector_name", "merchant_id", "flow"]; + impl tracing_subscriber::registry::LookupSpan<'a>> Layer for StorageSubscription { /// On new span. fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { + #[allow(clippy::expect_used)] let span = ctx.span(id).expect("No span"); + let mut extensions = span.extensions_mut(); let mut visitor = if let Some(parent_span) = span.parent() { let mut extensions = parent_span.extensions_mut(); @@ -106,15 +113,16 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer Storage::default() }; - let mut extensions = span.extensions_mut(); attrs.record(&mut visitor); extensions.insert(visitor); } /// On additional key value pairs store it. fn on_record(&self, span: &Id, values: &Record<'_>, ctx: Context<'_, S>) { + #[allow(clippy::expect_used)] let span = ctx.span(span).expect("No span"); let mut extensions = span.extensions_mut(); + #[allow(clippy::expect_used)] let visitor = extensions .get_mut::>() .expect("The span does not have storage"); @@ -123,6 +131,7 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer /// On enter store time. fn on_enter(&self, span: &Id, ctx: Context<'_, S>) { + #[allow(clippy::expect_used)] let span = ctx.span(span).expect("No span"); let mut extensions = span.extensions_mut(); if extensions.get_mut::().is_none() { @@ -132,6 +141,7 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer /// On close create an entry about how long did it take. fn on_close(&self, span: Id, ctx: Context<'_, S>) { + #[allow(clippy::expect_used)] let span = ctx.span(&span).expect("No span"); let elapsed_milliseconds = { @@ -142,7 +152,20 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer .unwrap_or(0) }; + if let Some(s) = span.extensions().get::>() { + s.values.iter().for_each(|(k, v)| { + if PERSISTENT_KEYS.contains(k) { + span.parent().and_then(|p| { + p.extensions_mut() + .get_mut::>() + .map(|s| s.values.insert(k, v.to_owned())) + }); + } + }) + }; + let mut extensions_mut = span.extensions_mut(); + #[allow(clippy::expect_used)] let visitor = extensions_mut .get_mut::>() .expect("No visitor in extensions"); diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 3bfd1ef7d9f8..55e7bafd8e00 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -54,6 +54,8 @@ pub enum Tag { /// API Flow #[derive(Debug, Display, Clone, PartialEq, Eq)] pub enum Flow { + /// Deep health Check + DeepHealthCheck, /// Merchants account create flow. MerchantsAccountCreate, /// Merchants account retrieve flow. @@ -163,6 +165,16 @@ pub enum Flow { RefundsUpdate, /// Refunds list flow. RefundsList, + // Retrieve forex flow. + RetrieveForexFlow, + /// Toggles recon service for a merchant. + ReconMerchantUpdate, + /// Recon token request flow. + ReconTokenRequest, + /// Initial request for recon service. + ReconServiceRequest, + /// Recon token verification flow + ReconVerifyToken, /// Routing create flow, RoutingCreateConfig, /// Routing link config @@ -183,6 +195,12 @@ pub enum Flow { RoutingUpdateDefaultConfig, /// Routing delete config RoutingDeleteConfig, + /// Add record to blocklist + AddToBlocklist, + /// Delete record from blocklist + DeleteFromBlocklist, + /// List entries from blocklist + ListBlocklist, /// Incoming Webhook Receive IncomingWebhookReceive, /// Validate payment method flow @@ -223,6 +241,10 @@ pub enum Flow { PaymentLinkRetrieve, /// payment Link Initiate flow PaymentLinkInitiate, + /// Payment Link List flow + PaymentLinkList, + /// Payment Link Status + PaymentLinkStatus, /// Create a business profile BusinessProfileCreate, /// Update a business profile @@ -245,8 +267,90 @@ pub enum Flow { GsmRuleUpdate, /// Gsm Rule Delete flow GsmRuleDelete, + /// User Sign Up + UserSignUp, + /// User Sign Up + UserSignUpWithMerchantId, + /// User Sign In without invite checks + UserSignInWithoutInviteChecks, + /// User Sign In + UserSignIn, /// User connect account UserConnectAccount, + /// Upsert Decision Manager Config + DecisionManagerUpsertConfig, + /// Delete Decision Manager Config + DecisionManagerDeleteConfig, + /// Retrieve Decision Manager Config + DecisionManagerRetrieveConfig, + /// Manual payment fulfillment acknowledgement + FrmFulfillment, + /// Change password flow + ChangePassword, + /// Signout flow + Signout, + /// 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, + /// Get role from token + GetRoleFromToken, + /// Update user role + UpdateUserRole, + /// Create merchant account for user in a org + UserMerchantAccountCreate, + /// Generate Sample Data + GenerateSampleData, + /// Delete Sample Data + DeleteSampleData, + /// List merchant accounts for user + UserMerchantAccountList, + /// Get users for merchant account + GetUserDetails, + /// PaymentMethodAuth Link token create + PmAuthLinkTokenCreate, + /// PaymentMethodAuth Exchange token create + PmAuthExchangeToken, + /// Get reset password link + ForgotPassword, + /// Reset password using link + ResetPassword, + /// Invite users + InviteUser, + /// Invite multiple users + InviteMultipleUser, + /// Delete user role + DeleteUserRole, + /// Incremental Authorization flow + PaymentsIncrementalAuthorization, + /// Get action URL for connector onboarding + GetActionUrl, + /// Sync connector onboarding status + SyncOnboardingStatus, + /// Reset tracking id + ResetTrackingId, + /// Verify email token without invite checks + VerifyEmailWithoutInviteChecks, + /// Verify email Token + VerifyEmail, + /// Send verify email + VerifyEmailRequest, + /// Update user account details + UpdateUserAccountDetails, + /// Accept user invitation + AcceptInvitation, } /// diff --git a/crates/router_env/src/metrics.rs b/crates/router_env/src/metrics.rs index 14402a7a6e91..961e0d362205 100644 --- a/crates/router_env/src/metrics.rs +++ b/crates/router_env/src/metrics.rs @@ -82,3 +82,22 @@ macro_rules! histogram_metric_u64 { > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); }; } + +/// Create a [`Histogram`][Histogram] i64 metric with the specified name and an optional description, +/// associated with the specified meter. Note that the meter must be to a valid [`Meter`][Meter]. +/// +/// [Histogram]: opentelemetry::metrics::Histogram +/// [Meter]: opentelemetry::metrics::Meter +#[macro_export] +macro_rules! histogram_metric_i64 { + ($name:ident, $meter:ident) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.i64_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.i64_histogram($description).init()); + }; +} diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index e0b68c709e8d..7d4a7821fc94 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] @@ -13,14 +13,15 @@ kv_store = [] async-trait = "0.1.68" error-stack = "0.3.1" futures = "0.3.28" +num_cpus = "1.15.0" once_cell = "1.18.0" rand = "0.8.5" -serde = "1.0.163" -serde_json = "1.0.96" +serde = "1.0.193" +serde_json = "1.0.108" strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } # First party crates diff --git a/crates/scheduler/src/configs/defaults.rs b/crates/scheduler/src/configs/defaults.rs index 25eb19e24f2a..d17c20829ea4 100644 --- a/crates/scheduler/src/configs/defaults.rs +++ b/crates/scheduler/src/configs/defaults.rs @@ -6,6 +6,7 @@ impl Default for super::settings::SchedulerSettings { consumer: super::settings::ConsumerSettings::default(), graceful_shutdown_interval: 60000, loop_interval: 5000, + server: super::settings::Server::default(), } } } @@ -30,3 +31,13 @@ impl Default for super::settings::ConsumerSettings { } } } + +impl Default for super::settings::Server { + fn default() -> Self { + Self { + port: 8080, + workers: num_cpus::get_physical(), + host: "localhost".into(), + } + } +} diff --git a/crates/scheduler/src/configs/settings.rs b/crates/scheduler/src/configs/settings.rs index 56a9f4079ac0..723ef81e70c7 100644 --- a/crates/scheduler/src/configs/settings.rs +++ b/crates/scheduler/src/configs/settings.rs @@ -15,6 +15,15 @@ pub struct SchedulerSettings { pub consumer: ConsumerSettings, pub loop_interval: u64, pub graceful_shutdown_interval: u64, + pub server: Server, +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(default)] +pub struct Server { + pub port: u16, + pub workers: usize, + pub host: String, } #[derive(Debug, Clone, Deserialize)] diff --git a/crates/scheduler/src/configs/validations.rs b/crates/scheduler/src/configs/validations.rs index e9f6621b2a5a..06052f9ff6c7 100644 --- a/crates/scheduler/src/configs/validations.rs +++ b/crates/scheduler/src/configs/validations.rs @@ -19,6 +19,8 @@ impl super::settings::SchedulerSettings { self.producer.validate()?; + self.server.validate()?; + Ok(()) } } @@ -32,3 +34,13 @@ impl super::settings::ProducerSettings { }) } } + +impl super::settings::Server { + pub fn validate(&self) -> Result<(), ApplicationError> { + common_utils::fp_utils::when(self.host.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "server host must not be empty".into(), + )) + }) + } +} diff --git a/crates/scheduler/src/consumer.rs b/crates/scheduler/src/consumer.rs index 08899552704a..ccc943afba3c 100644 --- a/crates/scheduler/src/consumer.rs +++ b/crates/scheduler/src/consumer.rs @@ -40,9 +40,15 @@ pub async fn start_consumer( use rand::distributions::{Distribution, Uniform}; let mut rng = rand::thread_rng(); + + // TODO: this can be removed once rand-0.9 is released + // reference - https://github.com/rust-random/rand/issues/1326#issuecomment-1635331942 + #[allow(unknown_lints)] + #[allow(clippy::unnecessary_fallible_conversions)] let timeout = Uniform::try_from(0..=settings.loop_interval) .into_report() .change_context(errors::ProcessTrackerError::ConfigurationError)?; + tokio::time::sleep(Duration::from_millis(timeout.sample(&mut rng))).await; let mut interval = tokio::time::interval(Duration::from_millis(settings.loop_interval)); @@ -61,7 +67,7 @@ pub async fn start_consumer( let handle = signal.handle(); let task_handle = tokio::spawn(common_utils::signals::signal_handler(signal, tx)); - loop { + 'consumer: loop { match rx.try_recv() { Err(mpsc::error::TryRecvError::Empty) => { interval.tick().await; @@ -71,7 +77,7 @@ pub async fn start_consumer( continue; } - tokio::task::spawn(pt_utils::consumer_operation_handler( + pt_utils::consumer_operation_handler( state.clone(), settings.clone(), |err| { @@ -79,19 +85,23 @@ pub async fn start_consumer( }, sync::Arc::clone(&consumer_operation_counter), workflow_selector, - )); + ) + .await; } Ok(()) | Err(mpsc::error::TryRecvError::Disconnected) => { logger::debug!("Awaiting shutdown!"); rx.close(); - shutdown_interval.tick().await; - let active_tasks = consumer_operation_counter.load(atomic::Ordering::Acquire); - match active_tasks { - 0 => { - logger::info!("Terminating consumer"); - break; + loop { + shutdown_interval.tick().await; + let active_tasks = consumer_operation_counter.load(atomic::Ordering::Acquire); + logger::error!("{}", active_tasks); + match active_tasks { + 0 => { + logger::info!("Terminating consumer"); + break 'consumer; + } + _ => continue, } - _ => continue, } } } @@ -204,6 +214,7 @@ where T: SchedulerAppState, { tracing::Span::current().record("workflow_id", Uuid::new_v4().to_string()); + logger::info!("{:?}", process.name.as_ref()); let res = workflow_selector .trigger_workflow(&state.clone(), process.clone()) .await; diff --git a/crates/scheduler/src/metrics.rs b/crates/scheduler/src/metrics.rs index 134f5599b31d..ca4fb9ec2424 100644 --- a/crates/scheduler/src/metrics.rs +++ b/crates/scheduler/src/metrics.rs @@ -6,8 +6,6 @@ global_meter!(PT_METER, "PROCESS_TRACKER"); histogram_metric!(CONSUMER_STATS, PT_METER, "CONSUMER_OPS"); counter_metric!(PAYMENT_COUNT, PT_METER); // No. of payments created -counter_metric!(TASKS_ADDED_COUNT, PT_METER); // Tasks added to process tracker -counter_metric!(TASKS_RESET_COUNT, PT_METER); // Tasks reset in process tracker for requeue flow counter_metric!(TASKS_PICKED_COUNT, PT_METER); // Tasks picked by counter_metric!(BATCHES_CREATED, PT_METER); // Batches added to stream counter_metric!(BATCHES_CONSUMED, PT_METER); // Batches consumed by consumer diff --git a/crates/scheduler/src/producer.rs b/crates/scheduler/src/producer.rs index 52510e1842e0..bcf37cdf6f22 100644 --- a/crates/scheduler/src/producer.rs +++ b/crates/scheduler/src/producer.rs @@ -30,9 +30,15 @@ where use rand::distributions::{Distribution, Uniform}; let mut rng = rand::thread_rng(); + + // TODO: this can be removed once rand-0.9 is released + // reference - https://github.com/rust-random/rand/issues/1326#issuecomment-1635331942 + #[allow(unknown_lints)] + #[allow(clippy::unnecessary_fallible_conversions)] let timeout = Uniform::try_from(0..=scheduler_settings.loop_interval) .into_report() .change_context(errors::ProcessTrackerError::ConfigurationError)?; + tokio::time::sleep(Duration::from_millis(timeout.sample(&mut rng))).await; let mut interval = tokio::time::interval(std::time::Duration::from_millis( diff --git a/crates/scheduler/src/utils.rs b/crates/scheduler/src/utils.rs index 53f14bd1fb9c..32fd97fca334 100644 --- a/crates/scheduler/src/utils.rs +++ b/crates/scheduler/src/utils.rs @@ -252,7 +252,7 @@ pub async fn consumer_operation_handler( E: FnOnce(error_stack::Report), T: SchedulerAppState, { - consumer_operation_counter.fetch_add(1, atomic::Ordering::Release); + consumer_operation_counter.fetch_add(1, atomic::Ordering::SeqCst); let start_time = std_time::Instant::now(); match consumer::consumer_operations(&state, &settings, workflow_selector).await { @@ -263,7 +263,7 @@ pub async fn consumer_operation_handler( let duration = end_time.saturating_duration_since(start_time).as_secs_f64(); logger::debug!("Time taken to execute consumer_operation: {}s", duration); - let current_count = consumer_operation_counter.fetch_sub(1, atomic::Ordering::Release); + let current_count = consumer_operation_counter.fetch_sub(1, atomic::Ordering::SeqCst); logger::info!("Current tasks being executed: {}", current_count); } diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index 77589cc7d782..c39154d97622 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -38,10 +38,10 @@ error-stack = "0.3.1" futures = "0.3.28" http = "0.2.9" mime = "0.3.17" -moka = { version = "0.11.3", features = ["future"] } +moka = { version = "0.12", features = ["future"] } once_cell = "1.18.0" ring = "0.16.20" -serde = { version = "1.0.185", features = ["derive"] } -serde_json = "1.0.105" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" thiserror = "1.0.40" -tokio = { version = "1.28.2", features = ["rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["rt-multi-thread"] } 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/errors.rs b/crates/storage_impl/src/errors.rs index bc68986cb8ea..2adcdcf8d2e7 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -55,6 +55,8 @@ pub enum StorageError { SerializationFailed, #[error("MockDb error")] MockDbError, + #[error("Kafka error")] + KafkaError, #[error("Customer with this id is Redacted")] CustomerRedacted, #[error("Deserialization failure")] @@ -92,15 +94,7 @@ impl Into for &StorageError { key: None, } } - storage_errors::DatabaseError::NoFieldsToUpdate => { - DataStorageError::DatabaseError("No fields to update".to_string()) - } - storage_errors::DatabaseError::QueryGenerationFailed => { - DataStorageError::DatabaseError("Query generation failed".to_string()) - } - storage_errors::DatabaseError::Others => { - DataStorageError::DatabaseError("Unknown database error".to_string()) - } + err => DataStorageError::DatabaseError(error_stack::report!(*err)), }, StorageError::ValueNotFound(i) => DataStorageError::ValueNotFound(i.clone()), StorageError::DuplicateValue { entity, key } => DataStorageError::DuplicateValue { @@ -111,6 +105,7 @@ impl Into for &StorageError { StorageError::KVError => DataStorageError::KVError, StorageError::SerializationFailed => DataStorageError::SerializationFailed, StorageError::MockDbError => DataStorageError::MockDbError, + StorageError::KafkaError => DataStorageError::KafkaError, StorageError::CustomerRedacted => DataStorageError::CustomerRedacted, StorageError::DeserializationFailed => DataStorageError::DeserializationFailed, StorageError::EncryptionError => DataStorageError::EncryptionError, @@ -158,6 +153,26 @@ impl StorageError { } } +pub trait RedisErrorExt { + #[track_caller] + fn to_redis_failed_response(self, key: &str) -> error_stack::Report; +} + +impl RedisErrorExt for error_stack::Report { + fn to_redis_failed_response(self, key: &str) -> error_stack::Report { + match self.current_context() { + RedisError::NotFound => self.change_context(DataStorageError::ValueNotFound(format!( + "Data does not exist for key {key}", + ))), + RedisError::SetNxFailed => self.change_context(DataStorageError::DuplicateValue { + entity: "redis", + key: Some(key.to_string()), + }), + _ => self.change_context(DataStorageError::KVError), + } + } +} + impl_error_type!(EncryptionError, "Encryption error"); #[derive(Debug, thiserror::Error)] @@ -364,3 +379,57 @@ pub enum ConnectorError { #[error("Missing 3DS redirection payload: {field_name}")] MissingConnectorRedirectionPayload { field_name: &'static str }, } + +#[derive(Debug, thiserror::Error)] +pub enum HealthCheckDBError { + #[error("Error while connecting to database")] + DBError, + #[error("Error while writing to database")] + DBWriteError, + #[error("Error while reading element in the database")] + DBReadError, + #[error("Error while deleting element in the database")] + DBDeleteError, + #[error("Unpredictable error occurred")] + UnknownError, + #[error("Error in database transaction")] + TransactionError, + #[error("Error while executing query in Sqlx Analytics")] + SqlxAnalyticsError, + #[error("Error while executing query in Clickhouse Analytics")] + ClickhouseAnalyticsError, +} + +impl From for HealthCheckDBError { + fn from(error: diesel::result::Error) -> Self { + match error { + diesel::result::Error::DatabaseError(_, _) => Self::DBError, + + diesel::result::Error::RollbackErrorOnCommit { .. } + | diesel::result::Error::RollbackTransaction + | diesel::result::Error::AlreadyInTransaction + | diesel::result::Error::NotInTransaction + | diesel::result::Error::BrokenTransactionManager => Self::TransactionError, + + _ => Self::UnknownError, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum HealthCheckRedisError { + #[error("Failed to establish Redis connection")] + RedisConnectionError, + #[error("Failed to set key value in Redis")] + SetFailed, + #[error("Failed to get key value in Redis")] + GetFailed, + #[error("Failed to delete key value in Redis")] + DeleteFailed, +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum HealthCheckLockerError { + #[error("Failed to establish Locker connection")] + FailedToCallLocker, +} diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index dc0dea4bb59c..d31f05b4979b 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -225,6 +225,11 @@ impl KVRouterStore { .change_context(RedisError::JsonSerializationFailed)?, ) .await + .map(|_| metrics::KV_PUSHED_TO_DRAINER.add(&metrics::CONTEXT, 1, &[])) + .map_err(|err| { + metrics::KV_FAILED_TO_PUSH_TO_DRAINER.add(&metrics::CONTEXT, 1, &[]); + err + }) .change_context(RedisError::StreamAppendFailed) } } @@ -251,14 +256,6 @@ pub(crate) fn diesel_error_to_data_error( entity: "entity ", key: None, }, - diesel_models::errors::DatabaseError::NoFieldsToUpdate => { - StorageError::DatabaseError("No fields to update".to_string()) - } - diesel_models::errors::DatabaseError::QueryGenerationFailed => { - StorageError::DatabaseError("Query generation failed".to_string()) - } - diesel_models::errors::DatabaseError::Others => { - StorageError::DatabaseError("Others".to_string()) - } + _ => StorageError::DatabaseError(error_stack::report!(*diesel_error)), } } diff --git a/crates/storage_impl/src/lookup.rs b/crates/storage_impl/src/lookup.rs index bd045fedd379..c96e24515772 100644 --- a/crates/storage_impl/src/lookup.rs +++ b/crates/storage_impl/src/lookup.rs @@ -11,6 +11,7 @@ use redis_interface::SetnxReply; use crate::{ diesel_error_to_data_error, + errors::RedisErrorExt, redis::kv_store::{kv_wrapper, KvOperation}, utils::{self, try_redis_get_else_try_database_get}, DatabaseStore, KVRouterStore, RouterStore, @@ -97,7 +98,7 @@ impl ReverseLookupInterface for KVRouterStore { format!("reverse_lookup_{}", &created_rev_lookup.lookup_id), ) .await - .change_context(errors::StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&created_rev_lookup.lookup_id))? .try_into_setnx() { Ok(SetnxReply::KeySet) => Ok(created_rev_lookup), diff --git a/crates/storage_impl/src/metrics.rs b/crates/storage_impl/src/metrics.rs index 3310e458879b..29bca2a007b7 100644 --- a/crates/storage_impl/src/metrics.rs +++ b/crates/storage_impl/src/metrics.rs @@ -8,3 +8,5 @@ counter_metric!(KV_MISS, GLOBAL_METER); // No. of KV misses // Metrics for KV counter_metric!(KV_OPERATION_SUCCESSFUL, GLOBAL_METER); counter_metric!(KV_OPERATION_FAILED, GLOBAL_METER); +counter_metric!(KV_PUSHED_TO_DRAINER, GLOBAL_METER); +counter_metric!(KV_FAILED_TO_PUSH_TO_DRAINER, GLOBAL_METER); diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index 4cdf8e2456bb..a6ba763bd916 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -43,6 +43,8 @@ pub struct MockDb { pub organizations: Arc>>, pub users: Arc>>, pub user_roles: Arc>>, + pub authorizations: Arc>>, + pub dashboard_metadata: Arc>>, } impl MockDb { @@ -78,6 +80,8 @@ impl MockDb { organizations: Default::default(), users: Default::default(), user_roles: Default::default(), + authorizations: 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..24863ddc568d 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -97,7 +97,7 @@ impl PaymentAttemptInterface for MockDb { #[allow(clippy::as_conversions)] let id = payment_attempts.len() as i32; let time = common_utils::date_time::now(); - + let payment_attempt = payment_attempt.populate_derived_fields(); let payment_attempt = PaymentAttempt { id, payment_id: payment_attempt.payment_id, @@ -105,6 +105,7 @@ impl PaymentAttemptInterface for MockDb { attempt_id: payment_attempt.attempt_id, status: payment_attempt.status, amount: payment_attempt.amount, + net_amount: payment_attempt.net_amount, currency: payment_attempt.currency, save_to_locker: payment_attempt.save_to_locker, connector: payment_attempt.connector, @@ -144,6 +145,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 +206,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..3f892ed9fa7a 100644 --- a/crates/storage_impl/src/mock_db/payment_intent.rs +++ b/crates/storage_impl/src/mock_db/payment_intent.rs @@ -106,6 +106,11 @@ 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, + authorization_count: new.authorization_count, + fingerprint_id: new.fingerprint_id, + session_expiry: new.session_expiry, }; 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 3d00e2f2bf7a..ad52a8aaeabc 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1,8 +1,8 @@ use api_models::enums::{AuthenticationType, Connector, PaymentMethod, PaymentMethodType}; -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, fallback_reverse_lookup_not_found}; use data_models::{ errors, - mandates::{MandateAmountData, MandateDataType}, + mandates::{MandateAmountData, MandateDataType, MandateDetails, MandateTypeDetails}, payments::{ payment_attempt::{ PaymentAttempt, PaymentAttemptInterface, PaymentAttemptNew, PaymentAttemptUpdate, @@ -14,6 +14,7 @@ use data_models::{ use diesel_models::{ enums::{ MandateAmountData as DieselMandateAmountData, MandateDataType as DieselMandateType, + MandateDetails as DieselMandateDetails, MandateTypeDetails as DieselMandateTypeOrDetails, MerchantStorageScheme, }, kv, @@ -29,6 +30,7 @@ use router_env::{instrument, tracing}; use crate::{ diesel_error_to_data_error, + errors::RedisErrorExt, lookup::ReverseLookupInterface, redis::kv_store::{kv_wrapper, KvOperation}, utils::{pg_connection_read, pg_connection_write, try_redis_get_else_try_database_get}, @@ -115,6 +117,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, @@ -309,6 +332,7 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { + let payment_attempt = payment_attempt.populate_derived_fields(); let key = format!( "mid_{}_pid_{}", payment_attempt.merchant_id, payment_attempt.payment_id @@ -321,6 +345,7 @@ impl PaymentAttemptInterface for KVRouterStore { attempt_id: payment_attempt.attempt_id.clone(), status: payment_attempt.status, amount: payment_attempt.amount, + net_amount: payment_attempt.net_amount, currency: payment_attempt.currency, save_to_locker: payment_attempt.save_to_locker, connector: payment_attempt.connector.clone(), @@ -364,6 +389,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); @@ -376,6 +403,20 @@ impl PaymentAttemptInterface for KVRouterStore { }, }; + //Reverse lookup for attempt_id + let reverse_lookup = ReverseLookupNew { + lookup_id: format!( + "pa_{}_{}", + &created_attempt.merchant_id, &created_attempt.attempt_id, + ), + pk_id: key.clone(), + sk_id: field.clone(), + source: "payment_attempt".to_string(), + updated_by: storage_scheme.to_string(), + }; + self.insert_reverse_lookup(reverse_lookup, storage_scheme) + .await?; + match kv_wrapper::( self, KvOperation::HSetNx( @@ -386,7 +427,7 @@ impl PaymentAttemptInterface for KVRouterStore { &key, ) .await - .change_context(errors::StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&key))? .try_into_hsetnx() { Ok(HsetnxReply::KeyNotSet) => Err(errors::StorageError::DuplicateValue { @@ -394,23 +435,7 @@ impl PaymentAttemptInterface for KVRouterStore { key: Some(key), }) .into_report(), - Ok(HsetnxReply::KeySet) => { - //Reverse lookup for attempt_id - let reverse_lookup = ReverseLookupNew { - lookup_id: format!( - "{}_{}", - &created_attempt.merchant_id, &created_attempt.attempt_id, - ), - pk_id: key, - sk_id: field, - source: "payment_attempt".to_string(), - updated_by: storage_scheme.to_string(), - }; - self.insert_reverse_lookup(reverse_lookup, storage_scheme) - .await?; - - Ok(created_attempt) - } + Ok(HsetnxReply::KeySet) => Ok(created_attempt), Err(error) => Err(error.change_context(errors::StorageError::KVError)), } } @@ -457,16 +482,6 @@ impl PaymentAttemptInterface for KVRouterStore { }, }; - kv_wrapper::<(), _, _>( - self, - KvOperation::Hset::((&field, redis_value), redis_entry), - &key, - ) - .await - .change_context(errors::StorageError::KVError)? - .try_into_hset() - .change_context(errors::StorageError::KVError)?; - match ( old_connector_transaction_id, &updated_attempt.connector_transaction_id, @@ -526,6 +541,16 @@ impl PaymentAttemptInterface for KVRouterStore { (_, _) => {} } + kv_wrapper::<(), _, _>( + self, + KvOperation::Hset::((&field, redis_value), redis_entry), + &key, + ) + .await + .change_context(errors::StorageError::KVError)? + .try_into_hset() + .change_context(errors::StorageError::KVError)?; + Ok(updated_attempt) } } @@ -551,10 +576,20 @@ 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 = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_conn_trans_{merchant_id}_{connector_transaction_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( + connector_transaction_id, + payment_id, + merchant_id, + storage_scheme, + ) + .await + ); + let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -616,6 +651,57 @@ impl PaymentAttemptInterface for KVRouterStore { } } + 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 + } + } + } + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, @@ -633,10 +719,18 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_txn_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_conn_trans_{merchant_id}_{connector_txn_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_merchant_id_connector_txn_id( + merchant_id, + connector_txn_id, + storage_scheme, + ) + .await + ); let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -725,10 +819,19 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{attempt_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_{merchant_id}_{attempt_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_attempt_id_merchant_id( + attempt_id, + merchant_id, + storage_scheme, + ) + .await + ); + let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( async { @@ -772,10 +875,18 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{preprocessing_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_preprocessing_{merchant_id}_{preprocessing_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_preprocessing_id_merchant_id( + preprocessing_id, + merchant_id, + storage_scheme, + ) + .await + ); let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -821,12 +932,23 @@ impl PaymentAttemptInterface for KVRouterStore { } MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); - - kv_wrapper(self, KvOperation::::Scan("pa_*"), key) - .await - .change_context(errors::StorageError::KVError)? - .try_into_scan() - .change_context(errors::StorageError::KVError) + Box::pin(try_redis_get_else_try_database_get( + async { + kv_wrapper(self, KvOperation::::Scan("pa_*"), key) + .await? + .try_into_scan() + }, + || async { + self.router_store + .find_attempts_by_merchant_id_payment_id( + merchant_id, + payment_id, + storage_scheme, + ) + .await + }, + )) + .await } } } @@ -889,6 +1011,50 @@ impl DataModelExt for MandateAmountData { } } } +impl DataModelExt for MandateDetails { + type StorageModel = DieselMandateDetails; + fn to_storage_model(self) -> Self::StorageModel { + DieselMandateDetails { + update_mandate_id: self.update_mandate_id, + mandate_type: self + .mandate_type + .map(|mand_type| mand_type.to_storage_model()), + } + } + fn from_storage_model(storage_model: Self::StorageModel) -> Self { + Self { + update_mandate_id: storage_model.update_mandate_id, + mandate_type: storage_model + .mandate_type + .map(MandateDataType::from_storage_model), + } + } +} +impl DataModelExt for MandateTypeDetails { + type StorageModel = DieselMandateTypeOrDetails; + + fn to_storage_model(self) -> Self::StorageModel { + match self { + Self::MandateType(mandate_type) => { + DieselMandateTypeOrDetails::MandateType(mandate_type.to_storage_model()) + } + Self::MandateDetails(mandate_details) => { + DieselMandateTypeOrDetails::MandateDetails(mandate_details.to_storage_model()) + } + } + } + + fn from_storage_model(storage_model: Self::StorageModel) -> Self { + match storage_model { + DieselMandateTypeOrDetails::MandateType(data) => { + Self::MandateType(MandateDataType::from_storage_model(data)) + } + DieselMandateTypeOrDetails::MandateDetails(data) => { + Self::MandateDetails(MandateDetails::from_storage_model(data)) + } + } + } +} impl DataModelExt for MandateDataType { type StorageModel = DieselMandateType; @@ -927,6 +1093,7 @@ impl DataModelExt for PaymentAttempt { attempt_id: self.attempt_id, status: self.status, amount: self.amount, + net_amount: Some(self.net_amount), currency: self.currency, save_to_locker: self.save_to_locker, connector: self.connector, @@ -966,11 +1133,14 @@ 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, } } fn from_storage_model(storage_model: Self::StorageModel) -> Self { Self { + net_amount: storage_model.get_or_calculate_net_amount(), id: storage_model.id, payment_id: storage_model.payment_id, merchant_id: storage_model.merchant_id, @@ -1009,7 +1179,7 @@ impl DataModelExt for PaymentAttempt { preprocessing_step_id: storage_model.preprocessing_step_id, mandate_details: storage_model .mandate_details - .map(MandateDataType::from_storage_model), + .map(MandateTypeDetails::from_storage_model), error_reason: storage_model.error_reason, multiple_capture_count: storage_model.multiple_capture_count, connector_response_reference_id: storage_model.connector_response_reference_id, @@ -1018,6 +1188,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, } } } @@ -1027,6 +1199,7 @@ impl DataModelExt for PaymentAttemptNew { fn to_storage_model(self) -> Self::StorageModel { DieselPaymentAttemptNew { + net_amount: Some(self.net_amount), payment_id: self.payment_id, merchant_id: self.merchant_id, attempt_id: self.attempt_id, @@ -1070,11 +1243,14 @@ 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, } } fn from_storage_model(storage_model: Self::StorageModel) -> Self { Self { + net_amount: storage_model.get_or_calculate_net_amount(), payment_id: storage_model.payment_id, merchant_id: storage_model.merchant_id, attempt_id: storage_model.attempt_id, @@ -1111,7 +1287,7 @@ impl DataModelExt for PaymentAttemptNew { preprocessing_step_id: storage_model.preprocessing_step_id, mandate_details: storage_model .mandate_details - .map(MandateDataType::from_storage_model), + .map(MandateTypeDetails::from_storage_model), error_reason: storage_model.error_reason, connector_response_reference_id: storage_model.connector_response_reference_id, multiple_capture_count: storage_model.multiple_capture_count, @@ -1120,6 +1296,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, } } } @@ -1205,6 +1383,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, } => DieselPaymentAttemptUpdate::ConfirmUpdate { @@ -1224,6 +1404,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1251,10 +1433,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, @@ -1270,10 +1452,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, @@ -1307,6 +1489,9 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, + connector_transaction_id, } => DieselPaymentAttemptUpdate::ErrorUpdate { connector, status, @@ -1315,13 +1500,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, @@ -1373,6 +1563,13 @@ impl DataModelExt for PaymentAttemptUpdate { connector, updated_by, }, + Self::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + } => DieselPaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + }, } } @@ -1454,6 +1651,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, } => Self::ConfirmUpdate { @@ -1473,6 +1672,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1500,10 +1701,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, @@ -1519,10 +1720,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, @@ -1556,6 +1757,9 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, + connector_transaction_id, } => Self::ErrorUpdate { connector, status, @@ -1564,11 +1768,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, }, @@ -1622,6 +1831,13 @@ impl DataModelExt for PaymentAttemptUpdate { connector, updated_by, }, + DieselPaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + } => Self::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + }, } } } @@ -1637,7 +1853,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!("pa_conn_trans_{}_{}", merchant_id, connector_transaction_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), @@ -1659,7 +1875,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!("pa_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 c3b3d22ffe35..8d20dfe0f32b 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -38,6 +38,7 @@ use router_env::{instrument, tracing}; use crate::connection; use crate::{ diesel_error_to_data_error, + errors::RedisErrorExt, redis::kv_store::{kv_wrapper, KvOperation}, utils::{self, pg_connection_read, pg_connection_write}, DataModelExt, DatabaseStore, KVRouterStore, @@ -97,6 +98,11 @@ 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, + authorization_count: new.authorization_count, + fingerprint_id: new.fingerprint_id.clone(), + session_expiry: new.session_expiry, }; let redis_entry = kv::TypedSql { op: kv::DBOperation::Insert { @@ -114,7 +120,7 @@ impl PaymentIntentInterface for KVRouterStore { &key, ) .await - .change_context(StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&key))? .try_into_hsetnx() { Ok(HsetnxReply::KeyNotSet) => Err(StorageError::DuplicateValue { @@ -175,7 +181,7 @@ impl PaymentIntentInterface for KVRouterStore { &key, ) .await - .change_context(StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&key))? .try_into_hset() .change_context(StorageError::KVError)?; @@ -491,12 +497,13 @@ impl PaymentIntentInterface for crate::RouterStore { .map(PaymentIntent::from_storage_model) .collect::>() }) - .into_report() .map_err(|er| { - let new_err = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_err) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable_lazy(|| "Error filtering records by predicate") + .into_report() } #[cfg(feature = "olap")] @@ -643,12 +650,13 @@ impl PaymentIntentInterface for crate::RouterStore { }) .collect() }) - .into_report() .map_err(|er| { - let new_er = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_er) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable("Error filtering payment records") + .into_report() } #[cfg(feature = "olap")] @@ -709,12 +717,13 @@ impl PaymentIntentInterface for crate::RouterStore { db_metrics::DatabaseOperation::Filter, ) .await - .into_report() .map_err(|er| { - let new_err = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_err) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable_lazy(|| "Error filtering records by predicate") + .into_report() } } @@ -758,6 +767,11 @@ 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, + authorization_count: self.authorization_count, + fingerprint_id: self.fingerprint_id, + session_expiry: self.session_expiry, } } @@ -798,6 +812,11 @@ 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, + authorization_count: storage_model.authorization_count, + fingerprint_id: storage_model.fingerprint_id, + session_expiry: storage_model.session_expiry, } } } @@ -843,6 +862,11 @@ 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, + authorization_count: self.authorization_count, + fingerprint_id: self.fingerprint_id, + session_expiry: self.session_expiry, } } @@ -884,6 +908,11 @@ 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, + authorization_count: storage_model.authorization_count, + fingerprint_id: storage_model.fingerprint_id, + session_expiry: storage_model.session_expiry, } } } @@ -898,11 +927,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 +968,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, @@ -958,6 +995,8 @@ impl DataModelExt for PaymentIntentUpdate { metadata, payment_confirm_source, updated_by, + fingerprint_id, + session_expiry, } => DieselPaymentIntentUpdate::Update { amount, currency, @@ -976,6 +1015,8 @@ impl DataModelExt for PaymentIntentUpdate { metadata, payment_confirm_source, updated_by, + fingerprint_id, + session_expiry, }, Self::PaymentAttemptAndAttemptCountUpdate { active_attempt_id, @@ -1020,6 +1061,14 @@ impl DataModelExt for PaymentIntentUpdate { surcharge_applicable: Some(surcharge_applicable), updated_by, }, + Self::IncrementalAuthorizationAmountUpdate { amount } => { + DieselPaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } + } + Self::AuthorizationCountUpdate { + authorization_count, + } => DieselPaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + }, } } diff --git a/crates/storage_impl/src/redis/cache.rs b/crates/storage_impl/src/redis/cache.rs index cd6066627620..a960261f8674 100644 --- a/crates/storage_impl/src/redis/cache.rs +++ b/crates/storage_impl/src/redis/cache.rs @@ -109,7 +109,6 @@ impl Cache { /// `max_capacity`: Max size in MB's that the cache can hold pub fn new(time_to_live: u64, time_to_idle: u64, max_capacity: Option) -> Self { let mut cache_builder = MokaCache::builder() - .eviction_listener_with_queued_delivery_mode(|_, _, _| {}) .time_to_live(std::time::Duration::from_secs(time_to_live)) .time_to_idle(std::time::Duration::from_secs(time_to_idle)); @@ -126,8 +125,8 @@ impl Cache { self.insert(key, Arc::new(val)).await; } - pub fn get_val(&self, key: &str) -> Option { - let val = self.get(key)?; + pub async fn get_val(&self, key: &str) -> Option { + let val = self.get(key).await?; (*val).as_any().downcast_ref::().cloned() } @@ -188,7 +187,7 @@ where F: FnOnce() -> Fut + Send, Fut: futures::Future> + Send, { - let cache_val = cache.get_val::(key); + let cache_val = cache.get_val::(key).await; if let Some(val) = cache_val { Ok(val) } else { @@ -266,14 +265,17 @@ mod cache_tests { async fn construct_and_get_cache() { let cache = Cache::new(1800, 1800, None); cache.push("key".to_string(), "val".to_string()).await; - assert_eq!(cache.get_val::("key"), Some(String::from("val"))); + assert_eq!( + cache.get_val::("key").await, + Some(String::from("val")) + ); } #[tokio::test] async fn eviction_on_size_test() { let cache = Cache::new(2, 2, Some(0)); cache.push("key".to_string(), "val".to_string()).await; - assert_eq!(cache.get_val::("key"), None); + assert_eq!(cache.get_val::("key").await, None); } #[tokio::test] @@ -283,7 +285,7 @@ mod cache_tests { cache.remove("key").await; - assert_eq!(cache.get_val::("key"), None); + assert_eq!(cache.get_val::("key").await, None); } #[tokio::test] @@ -291,6 +293,6 @@ mod cache_tests { let cache = Cache::new(2, 2, None); cache.push("key".to_string(), "val".to_string()).await; tokio::time::sleep(std::time::Duration::from_secs(3)).await; - assert_eq!(cache.get_val::("key"), None); + assert_eq!(cache.get_val::("key").await, None); } } diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index 3eadd8b83ade..ad57ad403d4a 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), @@ -130,7 +131,16 @@ where } KvOperation::Scan(pattern) => { - let result: Vec = redis_conn.hscan_and_deserialize(key, pattern, None).await?; + let result: Vec = redis_conn + .hscan_and_deserialize(key, pattern, None) + .await + .and_then(|result| { + if result.is_empty() { + Err(RedisError::NotFound).into_report() + } else { + Ok(result) + } + })?; Ok(KvResult::Scan(result)) } @@ -145,8 +155,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 +172,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/crates/storage_impl/src/utils.rs b/crates/storage_impl/src/utils.rs index 6d6e1cd5402b..6d69f02593fd 100644 --- a/crates/storage_impl/src/utils.rs +++ b/crates/storage_impl/src/utils.rs @@ -3,7 +3,7 @@ use data_models::errors::StorageError; use diesel::PgConnection; use error_stack::{IntoReport, ResultExt}; -use crate::{metrics, DatabaseStore}; +use crate::{errors::RedisErrorExt, metrics, DatabaseStore}; pub async fn pg_connection_read( store: &T, @@ -64,7 +64,8 @@ where metrics::KV_MISS.add(&metrics::CONTEXT, 1, &[]); database_call_closure().await } - _ => Err(redis_error.change_context(StorageError::KVError)), + // Keeping the key empty here since the error would never go here. + _ => Err(redis_error.to_redis_failed_response("")), }, } } diff --git a/crates/test_utils/Cargo.toml b/crates/test_utils/Cargo.toml index 957a51171da7..d53cdc4ac074 100644 --- a/crates/test_utils/Cargo.toml +++ b/crates/test_utils/Cargo.toml @@ -18,13 +18,13 @@ base64 = "0.21.2" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } rand = "0.8.5" reqwest = { version = "0.11.18", features = ["native-tls"] } -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" serde_urlencoded = "0.7.1" serial_test = "2.0.0" thirtyfour = "0.31.0" time = { version = "0.3.21", features = ["macros"] } -tokio = "1.28.2" +tokio = "1.35.1" toml = "0.7.4" # First party crates diff --git a/crates/test_utils/README.md b/crates/test_utils/README.md index 2edbc7104c25..a82c74cb59f6 100644 --- a/crates/test_utils/README.md +++ b/crates/test_utils/README.md @@ -22,9 +22,9 @@ The heart of `newman`(with directory support) and `UI-tests` Required fields: -- `--admin_api_key` -- Admin API Key of the environment. `test_admin` is the Admin API Key for running locally -- `--base_url` -- Base URL of the environment. `http://127.0.0.1:8080` / `http://localhost:8080` is the Base URL for running locally -- `--connector_name` -- Name of the connector that you wish to run. Example: `adyen`, `shift4`, `stripe` +- `--admin-api-key` -- Admin API Key of the environment. `test_admin` is the Admin API Key for running locally +- `--base-url` -- Base URL of the environment. `http://127.0.0.1:8080` / `http://localhost:8080` is the Base URL for running locally +- `--connector-name` -- Name of the connector that you wish to run. Example: `adyen`, `shift4`, `stripe` Optional fields: @@ -46,7 +46,7 @@ Optional fields: - Tests can be run with the following command: ```shell - cargo run --package test_utils --bin test_utils -- --connector_name= --base_url= --admin_api_key= \ + cargo run --package test_utils --bin test_utils -- --connector-name= --base-url= --admin-api-key= \ # optionally --folder ",,..." --verbose ``` diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 9562972c126e..d95d6ed94f1f 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -48,6 +48,7 @@ pub struct ConnectorAuthentication { pub payme: Option, pub paypal: Option, pub payu: Option, + pub placetopay: Option, pub powertranz: Option, pub prophetpay: Option, pub rapyd: Option, diff --git a/crates/test_utils/tests/connectors/selenium.rs b/crates/test_utils/tests/connectors/selenium.rs index 865cd950f764..303d4cd7ccbf 100644 --- a/crates/test_utils/tests/connectors/selenium.rs +++ b/crates/test_utils/tests/connectors/selenium.rs @@ -397,7 +397,7 @@ pub trait SeleniumTest { ) -> Result<(), WebDriverError> { let config = self.get_configs().automation_configs.unwrap(); if config.run_minimum_steps.unwrap() { - self.complete_actions(&web_driver, actions[..3].to_vec()) + self.complete_actions(&web_driver, actions.get(..3).unwrap().to_vec()) .await } else { self.complete_actions(&web_driver, actions).await @@ -538,7 +538,7 @@ pub trait SeleniumTest { let response = client.get(outgoing_webhook_url).send().await.unwrap(); // get events from outgoing webhook endpoint let body_text = response.text().await.unwrap(); let data: WebhookResponse = serde_json::from_str(&body_text).unwrap(); - let last_three_events = &data.data[data.data.len().saturating_sub(3)..]; // Get the last three elements if available + let last_three_events = data.data.get(data.data.len().saturating_sub(3)..).unwrap(); // Get the last three elements if available for last_event in last_three_events { let last_event_body = &last_event.step.request.body; let decoded_bytes = base64::engine::general_purpose::STANDARD //decode the encoded outgoing webhook event @@ -762,7 +762,7 @@ macro_rules! function { std::any::type_name::() } let name = type_name_of(f); - &name[..name.len() - 3] + &name.get(..name.len() - 3).unwrap() }}; } @@ -812,8 +812,14 @@ pub fn should_ignore_test(name: &str) -> bool { let tests_to_ignore: HashSet = serde_json::from_value(conf).unwrap_or_else(|_| HashSet::new()); let modules: Vec<_> = name.split("::").collect(); - let file_match = format!("{}::*", <&str>::clone(&modules[1])); - let module_name = modules[1..3].join("::"); + let file_match = format!( + "{}::*", + <&str>::clone(modules.get(1).expect("Error obtaining module path segment")) + ); + let module_name = modules + .get(1..3) + .expect("Error obtaining module path segment") + .join("::"); // Ignore if it matches patterns like nuvei_ui::*, nuvei_ui::should_make_nuvei_eps_payment_test tests_to_ignore.contains(&file_match) || tests_to_ignore.contains(&module_name) } diff --git a/crates/test_utils/tests/sample_auth.toml b/crates/test_utils/tests/sample_auth.toml index 0ae7c40d42d3..08b24817c24e 100644 --- a/crates/test_utils/tests/sample_auth.toml +++ b/crates/test_utils/tests/sample_auth.toml @@ -108,7 +108,7 @@ api_key = "API Key" [iatapay] key1 = "key1" api_key = "api_key" -api_secret = "secrect" +api_secret = "secret" [dummyconnector] api_key = "API Key" diff --git a/docker-compose-development.yml b/docker-compose-development.yml new file mode 100644 index 000000000000..665bdf2f05db --- /dev/null +++ b/docker-compose-development.yml @@ -0,0 +1,297 @@ +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 + networks: + - router_net + ports: + - "6379: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 + 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..e55008f1e34e 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,58 @@ services: - POSTGRES_PASSWORD=db_pass - POSTGRES_DB=hyperswitch_db + redis-standalone: + image: redis:7 + networks: + - router_net + ports: + - "6379: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 + - ./files:/local/bin/files 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,60 +82,66 @@ 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: - - redis networks: - router_net ports: - "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 +178,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 +259,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/docker/wasm-build.Dockerfile b/docker/wasm-build.Dockerfile new file mode 100644 index 000000000000..2ebdcec217ef --- /dev/null +++ b/docker/wasm-build.Dockerfile @@ -0,0 +1,25 @@ +FROM rust:latest as builder + +ARG RUN_ENV=sandbox +ARG EXTRA_FEATURES="" + +RUN apt-get update \ + && apt-get install -y libssl-dev pkg-config + +ENV CARGO_INCREMENTAL=0 +# Allow more retries for network requests in cargo (downloading crates) and +# rustup (installing toolchains). This should help to reduce flaky CI failures +# from transient network timeouts or other issues. +ENV CARGO_NET_RETRY=10 +ENV RUSTUP_MAX_RETRIES=10 +# Don't emit giant backtraces in the CI logs. +ENV RUST_BACKTRACE="short" +ENV env=$env +COPY . . +RUN echo env +RUN cargo install wasm-pack +RUN wasm-pack build --target web --out-dir /tmp/wasm --out-name euclid crates/euclid_wasm -- --features ${RUN_ENV},${EXTRA_FEATURES} + +FROM scratch + +COPY --from=builder /tmp/wasm /tmp 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 2fb729fb7b90..954336070d58 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -33,6 +33,16 @@ host = "" host_rs = "" mock_locker = true basilisk_host = "" +locker_enabled = true + +[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 @@ -42,12 +52,6 @@ max_attempts = 10 max_age = 365 [jwekey] -locker_key_identifier1 = "" -locker_key_identifier2 = "" -locker_encryption_key1 = "" -locker_encryption_key2 = "" -locker_decryption_key1 = "" -locker_decryption_key2 = "" vault_encryption_key = "" rust_locker_encryption_key = "" vault_private_key = "" @@ -102,10 +106,13 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" @@ -163,6 +170,7 @@ cards = [ "payme", "paypal", "payu", + "placetopay", "powertranz", "prophetpay", "shift4", @@ -193,7 +201,7 @@ red_pagos = { country = "UY", currency = "UYU" } #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" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } mollie = {long_lived_token = false, payment_method = "card"} braintree = { long_lived_token = false, payment_method = "card" } gocardless = {long_lived_token = true, payment_method = "bank_debit"} @@ -233,11 +241,13 @@ pay_later.klarna = {connector_list = "adyen"} wallet.google_pay = {connector_list = "stripe,adyen"} wallet.apple_pay = {connector_list = "stripe,adyen"} wallet.paypal = {connector_list = "adyen"} -card.credit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} -card.debit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} +card.credit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} +card.debit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} [analytics] source = "sqlx" @@ -253,3 +263,11 @@ connection_timeout = 10 [kv_config] ttl = 300 # 5 * 60 seconds + +[frm] +enabled = true + +[connector_onboarding.paypal] +client_id = "" +client_secret = "" +partner_id = "" 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..4a74afb9ad0e --- /dev/null +++ b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql @@ -0,0 +1,21 @@ +-- 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-24-112541_add_payment_config_business_profile/down.sql b/migrations/2023-11-24-112541_add_payment_config_business_profile/down.sql new file mode 100644 index 000000000000..16705c2bf56f --- /dev/null +++ b/migrations/2023-11-24-112541_add_payment_config_business_profile/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE business_profile DROP COLUMN IF EXISTS payment_link_config; \ No newline at end of file diff --git a/migrations/2023-11-24-112541_add_payment_config_business_profile/up.sql b/migrations/2023-11-24-112541_add_payment_config_business_profile/up.sql new file mode 100644 index 000000000000..40e65c149f26 --- /dev/null +++ b/migrations/2023-11-24-112541_add_payment_config_business_profile/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE business_profile +ADD COLUMN IF NOT EXISTS payment_link_config JSONB DEFAULT NULL; diff --git a/migrations/2023-11-24-115538_add_profile_id_payment_link/down.sql b/migrations/2023-11-24-115538_add_profile_id_payment_link/down.sql new file mode 100644 index 000000000000..4fa87f5f9318 --- /dev/null +++ b/migrations/2023-11-24-115538_add_profile_id_payment_link/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_link DROP COLUMN profile_id; \ No newline at end of file diff --git a/migrations/2023-11-24-115538_add_profile_id_payment_link/up.sql b/migrations/2023-11-24-115538_add_profile_id_payment_link/up.sql new file mode 100644 index 000000000000..207fdc8817e1 --- /dev/null +++ b/migrations/2023-11-24-115538_add_profile_id_payment_link/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_link ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64) DEFAULT NULL; 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/migrations/2023-11-30-170902_add-authorizations-table/down.sql b/migrations/2023-11-30-170902_add-authorizations-table/down.sql new file mode 100644 index 000000000000..476f16a52aab --- /dev/null +++ b/migrations/2023-11-30-170902_add-authorizations-table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS incremental_authorization; \ No newline at end of file diff --git a/migrations/2023-11-30-170902_add-authorizations-table/up.sql b/migrations/2023-11-30-170902_add-authorizations-table/up.sql new file mode 100644 index 000000000000..ade615877dc2 --- /dev/null +++ b/migrations/2023-11-30-170902_add-authorizations-table/up.sql @@ -0,0 +1,16 @@ +-- Your SQL goes here + +CREATE TABLE IF NOT EXISTS incremental_authorization ( + authorization_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + payment_id VARCHAR(64) NOT NULL, + amount BIGINT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + status VARCHAR(64) NOT NULL, + error_code VARCHAR(255), + error_message TEXT, + connector_authorization_id VARCHAR(64), + previously_authorized_amount BIGINT NOT NULL, + PRIMARY KEY (authorization_id, merchant_id) +); \ No newline at end of file diff --git a/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql new file mode 100644 index 000000000000..8d4d0e6d81d2 --- /dev/null +++ b/migrations/2023-12-01-090834_add-authorization_count-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 authorization_count; \ No newline at end of file diff --git a/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql new file mode 100644 index 000000000000..741135c6a1a3 --- /dev/null +++ b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS authorization_count INTEGER; diff --git a/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/down.sql b/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/down.sql new file mode 100644 index 000000000000..6af3e1e7f3df --- /dev/null +++ b/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS session_expiry; diff --git a/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/up.sql b/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/up.sql new file mode 100644 index 000000000000..e6ad0a728d44 --- /dev/null +++ b/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS session_expiry TIMESTAMP DEFAULT NULL; diff --git a/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/down.sql b/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/down.sql new file mode 100644 index 000000000000..1e7311bedbc5 --- /dev/null +++ b/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent ALTER COLUMN request_incremental_authorization SET NOT NULL; \ No newline at end of file diff --git a/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/up.sql b/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/up.sql new file mode 100644 index 000000000000..af2b897aa24b --- /dev/null +++ b/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ALTER COLUMN request_incremental_authorization DROP NOT NULL; \ No newline at end of file diff --git a/migrations/2023-12-11-075542_create_pm_fingerprint_table/down.sql b/migrations/2023-12-11-075542_create_pm_fingerprint_table/down.sql new file mode 100644 index 000000000000..74c450622a7e --- /dev/null +++ b/migrations/2023-12-11-075542_create_pm_fingerprint_table/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE blocklist_fingerprint; + +DROP TYPE "BlocklistDataKind"; diff --git a/migrations/2023-12-11-075542_create_pm_fingerprint_table/up.sql b/migrations/2023-12-11-075542_create_pm_fingerprint_table/up.sql new file mode 100644 index 000000000000..417d779200fc --- /dev/null +++ b/migrations/2023-12-11-075542_create_pm_fingerprint_table/up.sql @@ -0,0 +1,19 @@ +-- Your SQL goes here + +CREATE TYPE "BlocklistDataKind" AS ENUM ( + 'payment_method', + 'card_bin', + 'extended_card_bin' +); + +CREATE TABLE blocklist_fingerprint ( + id SERIAL PRIMARY KEY, + merchant_id VARCHAR(64) NOT NULL, + fingerprint_id VARCHAR(64) NOT NULL, + data_kind "BlocklistDataKind" NOT NULL, + encrypted_fingerprint TEXT NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE UNIQUE INDEX blocklist_fingerprint_merchant_id_fingerprint_id_index +ON blocklist_fingerprint (merchant_id, fingerprint_id); diff --git a/migrations/2023-12-12-112941_create_pm_blocklist_table/down.sql b/migrations/2023-12-12-112941_create_pm_blocklist_table/down.sql new file mode 100644 index 000000000000..cd7d412aad96 --- /dev/null +++ b/migrations/2023-12-12-112941_create_pm_blocklist_table/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE blocklist; diff --git a/migrations/2023-12-12-112941_create_pm_blocklist_table/up.sql b/migrations/2023-12-12-112941_create_pm_blocklist_table/up.sql new file mode 100644 index 000000000000..6d921dd78c30 --- /dev/null +++ b/migrations/2023-12-12-112941_create_pm_blocklist_table/up.sql @@ -0,0 +1,13 @@ +-- Your SQL goes here + +CREATE TABLE blocklist ( + id SERIAL PRIMARY KEY, + merchant_id VARCHAR(64) NOT NULL, + fingerprint_id VARCHAR(64) NOT NULL, + data_kind "BlocklistDataKind" NOT NULL, + metadata JSONB, + created_at TIMESTAMP NOT NULL +); + +CREATE UNIQUE INDEX blocklist_unique_fingerprint_id_index ON blocklist (merchant_id, fingerprint_id); +CREATE INDEX blocklist_merchant_id_data_kind_created_at_index ON blocklist (merchant_id, data_kind, created_at DESC); diff --git a/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/down.sql b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/down.sql new file mode 100644 index 000000000000..46b871b6ee4c --- /dev/null +++ b/migrations/2023-12-12-113330_add_fingerprint_id_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 fingerprint_id; diff --git a/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/up.sql b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/up.sql new file mode 100644 index 000000000000..831fb7b6ffc7 --- /dev/null +++ b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS fingerprint_id VARCHAR(64); diff --git a/migrations/2023-12-14-060824_user_roles_user_status_column/down.sql b/migrations/2023-12-14-060824_user_roles_user_status_column/down.sql new file mode 100644 index 000000000000..a246675a7d00 --- /dev/null +++ b/migrations/2023-12-14-060824_user_roles_user_status_column/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE user_roles RENAME COLUMN last_modified TO last_modified_at; +ALTER TABLE user_roles ALTER COLUMN status TYPE VARCHAR(64) USING (status::text); +DROP TYPE IF EXISTS "UserStatus"; diff --git a/migrations/2023-12-14-060824_user_roles_user_status_column/up.sql b/migrations/2023-12-14-060824_user_roles_user_status_column/up.sql new file mode 100644 index 000000000000..4d0245fcc3a1 --- /dev/null +++ b/migrations/2023-12-14-060824_user_roles_user_status_column/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TABLE user_roles RENAME COLUMN last_modified_at TO last_modified; +CREATE TYPE "UserStatus" AS ENUM ('active', 'invitation_sent'); +ALTER TABLE user_roles ALTER COLUMN status TYPE "UserStatus" USING (status::"UserStatus"); diff --git a/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/down.sql b/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/down.sql new file mode 100644 index 000000000000..bd2a8e1060a5 --- /dev/null +++ b/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE dashboard_metadata ALTER COLUMN data_key TYPE VARCHAR(64); +DROP TYPE IF EXISTS "DashboardMetadata"; diff --git a/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql b/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql new file mode 100644 index 000000000000..653bee36866d --- /dev/null +++ b/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql @@ -0,0 +1,25 @@ +-- Your SQL goes here +CREATE TYPE "DashboardMetadata" AS ENUM ( + 'production_agreement', + 'setup_processor', + 'configure_endpoint', + 'setup_complete', + 'first_processor_connected', + 'second_processor_connected', + 'configured_routing', + 'test_payment', + 'integration_method', + 'stripe_connected', + 'paypal_connected', + 'sp_routing_configured', + 'sp_test_payment', + 'download_woocom', + 'configure_woocom', + 'setup_woocom_webhook', + 'is_multiple_configuration', + 'configuration_type', + 'feedback', + 'prod_intent' +); + +ALTER TABLE dashboard_metadata ALTER COLUMN data_key TYPE "DashboardMetadata" USING (data_key::"DashboardMetadata"); \ No newline at end of file diff --git a/migrations/2023-12-15-062816__net_amount_in_payment_attempt/down.sql b/migrations/2023-12-15-062816__net_amount_in_payment_attempt/down.sql new file mode 100644 index 000000000000..93cd6341a9bd --- /dev/null +++ b/migrations/2023-12-15-062816__net_amount_in_payment_attempt/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS net_amount; \ No newline at end of file diff --git a/migrations/2023-12-15-062816__net_amount_in_payment_attempt/up.sql b/migrations/2023-12-15-062816__net_amount_in_payment_attempt/up.sql new file mode 100644 index 000000000000..fa86b097ef21 --- /dev/null +++ b/migrations/2023-12-15-062816__net_amount_in_payment_attempt/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE payment_attempt +ADD COLUMN IF NOT EXISTS net_amount BIGINT; +-- Backfill +UPDATE payment_attempt pa +SET net_amount = pa.amount + COALESCE(pa.surcharge_amount, 0) + COALESCE(pa.tax_amount, 0); diff --git a/migrations/2023-12-18-062613_create_blocklist_lookup_table/down.sql b/migrations/2023-12-18-062613_create_blocklist_lookup_table/down.sql new file mode 100644 index 000000000000..d2363f547a50 --- /dev/null +++ b/migrations/2023-12-18-062613_create_blocklist_lookup_table/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE blocklist_lookup; diff --git a/migrations/2023-12-18-062613_create_blocklist_lookup_table/up.sql b/migrations/2023-12-18-062613_create_blocklist_lookup_table/up.sql new file mode 100644 index 000000000000..8af3e209fc62 --- /dev/null +++ b/migrations/2023-12-18-062613_create_blocklist_lookup_table/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here + +CREATE TABLE blocklist_lookup ( + id SERIAL PRIMARY KEY, + merchant_id VARCHAR(64) NOT NULL, + fingerprint TEXT NOT NULL +); + +CREATE UNIQUE INDEX blocklist_lookup_merchant_id_fingerprint_index ON blocklist_lookup (merchant_id, fingerprint); diff --git a/migrations/2023-12-27-104559_business_profile_add_session_expiry/down.sql b/migrations/2023-12-27-104559_business_profile_add_session_expiry/down.sql new file mode 100644 index 000000000000..cdc7fe5b81c2 --- /dev/null +++ b/migrations/2023-12-27-104559_business_profile_add_session_expiry/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE business_profile DROP COLUMN IF EXISTS session_expiry; \ No newline at end of file diff --git a/migrations/2023-12-27-104559_business_profile_add_session_expiry/up.sql b/migrations/2023-12-27-104559_business_profile_add_session_expiry/up.sql new file mode 100644 index 000000000000..462dfd234abd --- /dev/null +++ b/migrations/2023-12-27-104559_business_profile_add_session_expiry/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS session_expiry BIGINT DEFAULT NULL; diff --git a/migrations/2023-12-28-063619_add_enum_types_to_EventType/down.sql b/migrations/2023-12-28-063619_add_enum_types_to_EventType/down.sql new file mode 100644 index 000000000000..c7c9cbeb4017 --- /dev/null +++ b/migrations/2023-12-28-063619_add_enum_types_to_EventType/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-12-28-063619_add_enum_types_to_EventType/up.sql b/migrations/2023-12-28-063619_add_enum_types_to_EventType/up.sql new file mode 100644 index 000000000000..74b87199c2fd --- /dev/null +++ b/migrations/2023-12-28-063619_add_enum_types_to_EventType/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TYPE "EventType" ADD VALUE IF NOT EXISTS 'payment_authorized'; +ALTER TYPE "EventType" ADD VALUE IF NOT EXISTS 'payment_captured'; diff --git a/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql b/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql new file mode 100644 index 000000000000..b9160b6f1052 --- /dev/null +++ b/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN preferred_merchant_id; diff --git a/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql b/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql new file mode 100644 index 000000000000..77567ce93faf --- /dev/null +++ b/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64); diff --git a/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/down.sql b/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/down.sql new file mode 100644 index 000000000000..c7c9cbeb4017 --- /dev/null +++ b/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/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/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/up.sql b/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/up.sql new file mode 100644 index 000000000000..bb703fde397f --- /dev/null +++ b/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TYPE "DashboardMetadata" ADD VALUE IF NOT EXISTS 'integration_completed'; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 9fddde01b49a..9afd5182529a 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -11,7 +11,7 @@ "license": { "name": "Apache-2.0" }, - "version": "0.2.0" + "version": "0.1.0" }, "servers": [ { @@ -26,7 +26,7 @@ "Payment Methods" ], "summary": "List payment methods for a Merchant", - "description": "List payment methods for a Merchant\n\nTo filter and list the applicable payment methods for a particular Merchant ID", + "description": "List payment methods for a Merchant\n\nLists the applicable payment methods for a particular Merchant ID.\nUse the client secret and publishable key authorization to list all relevant payment methods of the merchant for the payment corresponding to the client secret.", "operationId": "List all Payment Methods for a Merchant", "parameters": [ { @@ -75,7 +75,7 @@ { "name": "maximum_amount", "in": "query", - "description": "The maximum amount amount accepted for processing by the particular payment method.", + "description": "The maximum amount accepted for processing by the particular payment method.", "required": true, "schema": { "type": "integer", @@ -129,37 +129,38 @@ ] } }, - "/customers": { - "post": { + "/account/{account_id}/business_profile": { + "get": { "tags": [ - "Customers" + "Business Profile" ], - "summary": "Create Customer", - "description": "Create Customer\n\nCreate a customer object and store the customer details to be reused for future payments. Incase the customer already exists in the system, this API will respond with the customer details.", - "operationId": "Create a Customer", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerRequest" - } + "summary": "Business Profile - List", + "description": "Business Profile - List\n\nLists all the *business profiles* under a merchant", + "operationId": "List Business Profiles", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "Merchant Identifier", + "required": true, + "schema": { + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Customer Created", + "description": "Business profiles Retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/BusinessProfileResponse" + } } } } - }, - "400": { - "description": "Invalid data" } }, "security": [ @@ -167,169 +168,202 @@ "api_key": [] } ] - } - }, - "/customers/list": { + }, "post": { "tags": [ - "Customers List" + "Business Profile" ], - "summary": "List customers for a merchant", - "description": "List customers for a merchant\n\nTo filter and list the customers for a particular merchant id", - "operationId": "List all Customers for a Merchant", + "summary": "Business Profile - Create", + "description": "Business Profile - Create\n\nCreates a new *business profile* for a merchant", + "operationId": "Create A Business Profile", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessProfileCreate" + }, + "examples": { + "Create a business profile with minimal fields": { + "value": {} + }, + "Create a business profile with profile name": { + "value": { + "profile_name": "shoe_business" + } + } + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Customers retrieved", + "description": "Business Account Created", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomerResponse" - } + "$ref": "#/components/schemas/BusinessProfileResponse" } } } }, "400": { - "description": "Invalid Data" + "description": "Invalid data" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/customers/payment_methods": { + "/account/{account_id}/business_profile/{profile_id}": { "get": { "tags": [ - "Payment Methods" + "Business Profile" ], - "summary": "List payment methods for a Customer", - "description": "List payment methods for a Customer\n\nTo filter and list the applicable payment methods for a particular Customer ID", - "operationId": "List all Payment Methods for a Customer", + "summary": "Business Profile - Retrieve", + "description": "Business Profile - Retrieve\n\nRetrieve existing *business profile*", + "operationId": "Retrieve a Business Profile", "parameters": [ { - "name": "client-secret", + "name": "account_id", "in": "path", - "description": "A secret known only to your application and the authorization server", + "description": "The unique identifier for the merchant account", "required": true, "schema": { "type": "string" } }, { - "name": "customer_id", + "name": "profile_id", "in": "path", - "description": "The unique identifier for the customer account", + "description": "The unique identifier for the business profile", "required": true, "schema": { "type": "string" } - }, - { - "name": "accepted_country", - "in": "query", - "description": "The two-letter ISO currency code", - "required": true, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "accepted_currency", - "in": "path", - "description": "The three-letter ISO currency code", - "required": true, - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" + } + ], + "responses": { + "200": { + "description": "Business Profile Updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessProfileResponse" + } } } }, + "400": { + "description": "Invalid data" + } + }, + "security": [ { - "name": "minimum_amount", - "in": "query", - "description": "The minimum amount accepted for processing by the particular payment method.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "maximum_amount", - "in": "query", - "description": "The maximum amount amount accepted for processing by the particular payment method.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "Business Profile" + ], + "summary": "Business Profile - Update", + "description": "Business Profile - Update\n\nUpdate the *business profile*", + "operationId": "Update a Business Profile", + "parameters": [ { - "name": "recurring_payment_enabled", - "in": "query", - "description": "Indicates whether the payment method is eligible for recurring payments", + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", "required": true, "schema": { - "type": "boolean" + "type": "string" } }, { - "name": "installment_payment_enabled", - "in": "query", - "description": "Indicates whether the payment method is eligible for installment payments", + "name": "profile_id", + "in": "path", + "description": "The unique identifier for the business profile", "required": true, "schema": { - "type": "boolean" + "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessProfileCreate" + }, + "examples": { + "Update business profile with profile name fields": { + "value": { + "profile_name": "shoe_business" + } + } + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", + "description": "Business Profile Updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerPaymentMethodsListResponse" + "$ref": "#/components/schemas/BusinessProfileResponse" } } } }, "400": { - "description": "Invalid Data" - }, - "404": { - "description": "Payment Methods does not exist in records" + "description": "Invalid data" } }, "security": [ { - "publishable_key": [] + "api_key": [] } ] - } - }, - "/customers/{customer_id}": { - "get": { + }, + "delete": { "tags": [ - "Customers" + "Business Profile" ], - "summary": "Retrieve Customer", - "description": "Retrieve Customer\n\nRetrieve a customer's details.", - "operationId": "Retrieve a Customer", + "summary": "Business Profile - Delete", + "description": "Business Profile - Delete\n\nDelete the *business profile*", + "operationId": "Delete the Business Profile", "parameters": [ { - "name": "customer_id", + "name": "account_id", "in": "path", - "description": "The unique identifier for the Customer", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "profile_id", + "in": "path", + "description": "The unique identifier for the business profile", "required": true, "schema": { "type": "string" @@ -338,51 +372,60 @@ ], "responses": { "200": { - "description": "Customer Retrieved", + "description": "Business profiles Deleted", "content": { - "application/json": { + "text/plain": { "schema": { - "$ref": "#/components/schemas/CustomerResponse" + "type": "boolean" } } } }, - "404": { - "description": "Customer was not found" + "400": { + "description": "Invalid data" } }, "security": [ { "api_key": [] - }, - { - "ephemeral_key": [] } ] - }, + } + }, + "/accounts": { "post": { "tags": [ - "Customers" - ], - "summary": "Update Customer", - "description": "Update Customer\n\nUpdates the customer's details in a customer object.", - "operationId": "Update a Customer", - "parameters": [ - { - "name": "customer_id", - "in": "path", - "description": "The unique identifier for the Customer", - "required": true, - "schema": { - "type": "string" - } - } + "Merchant Account" ], + "summary": "Merchant Account - Create", + "description": "Merchant Account - Create\n\nCreate a new account for a *merchant* and the *merchant* could be a seller or retailer or client who likes to receive and send payments.", + "operationId": "Create a Merchant Account", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerRequest" + "$ref": "#/components/schemas/MerchantAccountCreate" + }, + "examples": { + "Create a merchant account with minimal fields": { + "value": { + "merchant_id": "merchant_abc" + } + }, + "Create a merchant account with return url": { + "value": { + "merchant_id": "merchant_abc", + "return_url": "https://example.com" + } + }, + "Create a merchant account with webhook url": { + "value": { + "merchant_id": "merchant_abc", + "webhook_details": { + "webhook_url": "https://webhook.site/a5c54f75-1f7e-4545-b781-af525b7e37a0" + } + } + } } } }, @@ -390,37 +433,39 @@ }, "responses": { "200": { - "description": "Customer was Updated", + "description": "Merchant Account Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerResponse" + "$ref": "#/components/schemas/MerchantAccountResponse" } } } }, - "404": { - "description": "Customer was not found" + "400": { + "description": "Invalid data" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] - }, - "delete": { + } + }, + "/accounts/{account_id}": { + "get": { "tags": [ - "Customers" + "Merchant Account" ], - "summary": "Delete Customer", - "description": "Delete Customer\n\nDelete a customer record.", - "operationId": "Delete a Customer", + "summary": "Merchant Account - Retrieve", + "description": "Merchant Account - Retrieve\n\nRetrieve a *merchant* account details.", + "operationId": "Retrieve a Merchant Account", "parameters": [ { - "name": "customer_id", + "name": "account_id", "in": "path", - "description": "The unique identifier for the Customer", + "description": "The unique identifier for the merchant account", "required": true, "schema": { "type": "string" @@ -429,294 +474,149 @@ ], "responses": { "200": { - "description": "Customer was Deleted", + "description": "Merchant Account Retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerDeleteResponse" + "$ref": "#/components/schemas/MerchantAccountResponse" } } } }, "404": { - "description": "Customer was not found" + "description": "Merchant account not found" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] - } - }, - "/customers/{customer_id}/payment_methods": { - "get": { + }, + "post": { "tags": [ - "Payment Methods" + "Merchant Account" ], - "summary": "List payment methods for a Customer", - "description": "List payment methods for a Customer\n\nTo filter and list the applicable payment methods for a particular Customer ID", - "operationId": "List all Payment Methods for a Customer", + "summary": "Merchant Account - Update", + "description": "Merchant Account - Update\n\nUpdates details of an existing merchant account. Helpful in updating merchant details such as email, contact details, or other configuration details like webhook, routing algorithm etc", + "operationId": "Update a Merchant Account", "parameters": [ { - "name": "customer_id", + "name": "account_id", "in": "path", - "description": "The unique identifier for the customer account", + "description": "The unique identifier for the merchant account", "required": true, "schema": { "type": "string" } - }, - { - "name": "accepted_country", - "in": "query", - "description": "The two-letter ISO currency code", - "required": true, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "accepted_currency", - "in": "path", - "description": "The three-letter ISO currency code", - "required": true, - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantAccountUpdate" + }, + "examples": { + "Update merchant name": { + "value": { + "merchant_id": "merchant_abc", + "merchant_name": "merchant_name" + } + }, + "Update return url": { + "value": { + "merchant_id": "merchant_abc", + "return_url": "https://example.com" + } + }, + "Update webhook url": { + "value": { + "merchant_id": "merchant_abc", + "webhook_details": { + "webhook_url": "https://webhook.site/a5c54f75-1f7e-4545-b781-af525b7e37a0" + } + } + } } } }, - { - "name": "minimum_amount", - "in": "query", - "description": "The minimum amount accepted for processing by the particular payment method.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "maximum_amount", - "in": "query", - "description": "The maximum amount amount accepted for processing by the particular payment method.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "recurring_payment_enabled", - "in": "query", - "description": "Indicates whether the payment method is eligible for recurring payments", - "required": true, - "schema": { - "type": "boolean" - } - }, - { - "name": "installment_payment_enabled", - "in": "query", - "description": "Indicates whether the payment method is eligible for installment payments", - "required": true, - "schema": { - "type": "boolean" - } - } - ], + "required": true + }, "responses": { "200": { - "description": "Payment Methods retrieved", + "description": "Merchant Account Updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerPaymentMethodsListResponse" + "$ref": "#/components/schemas/MerchantAccountResponse" } } } }, - "400": { - "description": "Invalid Data" - }, "404": { - "description": "Payment Methods does not exist in records" + "description": "Merchant account not found" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] - } - }, - "/disputes/list": { - "get": { + }, + "delete": { "tags": [ - "Disputes" + "Merchant Account" ], - "summary": "Disputes - List Disputes", - "description": "Disputes - List Disputes", - "operationId": "List Disputes", + "summary": "Merchant Account - Delete", + "description": "Merchant Account - Delete\n\nDelete a *merchant* account", + "operationId": "Delete a Merchant Account", "parameters": [ { - "name": "limit", - "in": "query", - "description": "The maximum number of Dispute Objects to include in the response", - "required": false, - "schema": { - "type": "integer", - "format": "int64", - "nullable": true - } - }, - { - "name": "dispute_status", - "in": "query", - "description": "The status of dispute", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/DisputeStatus" - } - ], - "nullable": true - } - }, - { - "name": "dispute_stage", - "in": "query", - "description": "The stage of dispute", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/DisputeStage" - } - ], - "nullable": true - } - }, - { - "name": "reason", - "in": "query", - "description": "The reason for dispute", - "required": false, - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "connector", - "in": "query", - "description": "The connector linked to dispute", - "required": false, - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "received_time", - "in": "query", - "description": "The time at which dispute is received", - "required": false, - "schema": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - { - "name": "received_time.lt", - "in": "query", - "description": "Time less than the dispute received time", - "required": false, - "schema": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - { - "name": "received_time.gt", - "in": "query", - "description": "Time greater than the dispute received time", - "required": false, - "schema": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - { - "name": "received_time.lte", - "in": "query", - "description": "Time less than or equals to the dispute received time", - "required": false, - "schema": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - { - "name": "received_time.gte", - "in": "query", - "description": "Time greater than or equals to the dispute received time", - "required": false, + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, "schema": { - "type": "string", - "format": "date-time", - "nullable": true + "type": "string" } } ], "responses": { "200": { - "description": "The dispute list was retrieved successfully", + "description": "Merchant Account Deleted", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DisputeResponse" - } + "$ref": "#/components/schemas/MerchantAccountDeleteResponse" } } } }, - "401": { - "description": "Unauthorized request" + "404": { + "description": "Merchant account not found" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/disputes/{dispute_id}": { + "/accounts/{account_id}/connectors": { "get": { "tags": [ - "Disputes" + "Merchant Connector Account" ], - "summary": "Disputes - Retrieve Dispute", - "description": "Disputes - Retrieve Dispute", - "operationId": "Retrieve a Dispute", + "summary": "Merchant Connector - List", + "description": "Merchant Connector - List\n\nList Merchant Connector Details for the merchant", + "operationId": "List all Merchant Connectors", "parameters": [ { - "name": "dispute_id", + "name": "account_id", "in": "path", - "description": "The identifier for dispute", + "description": "The unique identifier for the merchant account", "required": true, "schema": { "type": "string" @@ -725,51 +625,92 @@ ], "responses": { "200": { - "description": "The dispute was retrieved successfully", + "description": "Merchant Connector list retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DisputeResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } } } } }, + "401": { + "description": "Unauthorized request" + }, "404": { - "description": "Dispute does not exist in our records" + "description": "Merchant Connector does not exist in records" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] - } - }, - "/gsm": { + }, "post": { "tags": [ - "Gsm" + "Merchant Connector Account" ], - "summary": "Gsm - Create", - "description": "Gsm - Create\n\nTo create a Gsm Rule", - "operationId": "Create Gsm Rule", + "summary": "Merchant Connector - Create", + "description": "Merchant Connector - Create\n\nCreates a new Merchant Connector for the merchant account. The connector could be a payment processor/facilitator/acquirer or a provider of specialized services like Fraud/Accounting etc.", + "operationId": "Create a Merchant Connector", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmCreateRequest" - } + "$ref": "#/components/schemas/MerchantConnectorCreate" + }, + "examples": { + "Create a merchant account with custom connector label": { + "value": { + "connector_account_details": { + "api_key": "{{adyen-api-key}}", + "auth_type": "BodyKey", + "key1": "{{adyen_merchant_account}}" + }, + "connector_label": "EU_adyen", + "connector_name": "adyen", + "connector_type": "fiz_operations" + } + }, + "Create a merchant connector account under a specific business profile": { + "value": { + "connector_account_details": { + "api_key": "{{adyen-api-key}}", + "auth_type": "BodyKey", + "key1": "{{adyen_merchant_account}}" + }, + "connector_name": "adyen", + "connector_type": "fiz_operations", + "profile_id": "{{profile_id}}" + } + }, + "Create a merchant connector account with minimal fields": { + "value": { + "connector_account_details": { + "api_key": "{{adyen-api-key}}", + "auth_type": "BodyKey", + "key1": "{{adyen_merchant_account}}" + }, + "connector_name": "adyen", + "connector_type": "fiz_operations" + } + } + } } }, "required": true }, "responses": { "200": { - "description": "Gsm created", + "description": "Merchant Connector Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmResponse" + "$ref": "#/components/schemas/MerchantConnectorResponse" } } } @@ -785,37 +726,51 @@ ] } }, - "/gsm/delete": { - "post": { + "/accounts/{account_id}/connectors/{connector_id}": { + "get": { "tags": [ - "Gsm" + "Merchant Connector Account" ], - "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" - } + "summary": "Merchant Connector - Retrieve", + "description": "Merchant Connector - Retrieve\n\nRetrieves details of a Connector account", + "operationId": "Retrieve a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" } }, - "required": true - }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], "responses": { "200": { - "description": "Gsm deleted", + "description": "Merchant Connector retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmDeleteResponse" + "$ref": "#/components/schemas/MerchantConnectorResponse" } } } }, - "400": { - "description": "Missing Mandatory fields" + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" } }, "security": [ @@ -823,21 +778,59 @@ "admin_api_key": [] } ] - } - }, - "/gsm/get": { + }, "post": { "tags": [ - "Gsm" + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Update", + "description": "Merchant Connector - Update\n\nTo update an existing Merchant Connector account. Helpful in enabling/disabling different payment methods and other settings for the connector", + "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" + } + } ], - "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" + "$ref": "#/components/schemas/MerchantConnectorUpdate" + }, + "examples": { + "Enable card payment method": { + "value": { + "connector_type": "fiz_operations", + "payment_methods_enabled": [ + { + "payment_method": "card" + } + ] + } + }, + "Update webhook secret": { + "value": { + "connector_webhook_details": { + "merchant_secret": "{{webhook_secret}}" + } + } + } } } }, @@ -845,17 +838,20 @@ }, "responses": { "200": { - "description": "Gsm retrieved", + "description": "Merchant Connector Updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmResponse" + "$ref": "#/components/schemas/MerchantConnectorResponse" } } } }, - "400": { - "description": "Missing Mandatory fields" + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" } }, "security": [ @@ -863,39 +859,51 @@ "admin_api_key": [] } ] - } - }, - "/gsm/update": { - "post": { + }, + "delete": { "tags": [ - "Gsm" + "Merchant Connector Account" ], - "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" - } + "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" } }, - "required": true - }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], "responses": { "200": { - "description": "Gsm updated", + "description": "Merchant Connector Deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmResponse" + "$ref": "#/components/schemas/MerchantConnectorDeleteResponse" } } } }, - "400": { - "description": "Missing Mandatory fields" + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" } }, "security": [ @@ -905,60 +913,79 @@ ] } }, - "/mandates/revoke/{mandate_id}": { + "/api_keys/{merchant_id)": { "post": { "tags": [ - "Mandates" + "API Key" ], - "summary": "Mandates - Revoke Mandate", - "description": "Mandates - Revoke Mandate\n\nRevoke a mandate", - "operationId": "Revoke a Mandate", + "summary": "API Key - Create", + "description": "API Key - Create\n\nCreate a new API Key for accessing our APIs from your servers. The plaintext API Key will be\ndisplayed only once on creation, so ensure you store it securely.", + "operationId": "Create an API Key", "parameters": [ { - "name": "mandate_id", + "name": "merchant_id", "in": "path", - "description": "The identifier for mandate", + "description": "The unique identifier for the merchant account", "required": true, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "The mandate was revoked successfully", + "description": "API Key created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MandateRevokedResponse" + "$ref": "#/components/schemas/CreateApiKeyResponse" } } } }, "400": { - "description": "Mandate does not exist in our records" + "description": "Invalid data" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/mandates/{mandate_id}": { - "get": { + "/api_keys/{merchant_id)/{key_id}": { + "delete": { "tags": [ - "Mandates" + "API Key" ], - "summary": "Mandates - Retrieve Mandate", - "description": "Mandates - Retrieve Mandate\n\nRetrieve a mandate", - "operationId": "Retrieve a Mandate", + "summary": "API Key - Revoke", + "description": "API Key - Revoke\n\nRevoke the specified API Key. Once revoked, the API Key can no longer be used for\nauthenticating with our APIs.", + "operationId": "Revoke an API Key", "parameters": [ { - "name": "mandate_id", + "name": "merchant_id", "in": "path", - "description": "The identifier for mandate", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "key_id", + "in": "path", + "description": "The unique identifier for the API Key", "required": true, "schema": { "type": "string" @@ -967,93 +994,107 @@ ], "responses": { "200": { - "description": "The mandate was retrieved successfully", + "description": "API Key revoked", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MandateResponse" + "$ref": "#/components/schemas/RevokeApiKeyResponse" } } } }, "404": { - "description": "Mandate does not exist in our records" + "description": "API Key not found" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/payment_link/{payment_link_id}": { + "/api_keys/{merchant_id}/{key_id}": { "get": { "tags": [ - "Payments" + "API Key" ], - "summary": "Payments Link - Retrieve", - "description": "Payments Link - Retrieve\n\nTo retrieve the properties of a Payment Link. This may be used to get the status of a previously initiated payment or next action for an ongoing payment", - "operationId": "Retrieve a Payment Link", + "summary": "API Key - Retrieve", + "description": "API Key - Retrieve\n\nRetrieve information about the specified API Key.", + "operationId": "Retrieve an API Key", "parameters": [ { - "name": "payment_link_id", + "name": "merchant_id", "in": "path", - "description": "The identifier for payment link", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "key_id", + "in": "path", + "description": "The unique identifier for the API Key", "required": true, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RetrievePaymentLinkRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Gets details regarding payment link", + "description": "API Key retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RetrievePaymentLinkResponse" + "$ref": "#/components/schemas/RetrieveApiKeyResponse" } } } }, "404": { - "description": "No payment link found" + "description": "API Key not found" } }, "security": [ { - "api_key": [] - }, - { - "publishable_key": [] + "admin_api_key": [] } ] - } - }, - "/payment_methods": { + }, "post": { "tags": [ - "Payment Methods" + "API Key" + ], + "summary": "API Key - Update", + "description": "API Key - Update\n\nUpdate information for the specified API Key.", + "operationId": "Update an API Key", + "parameters": [ + { + "name": "merchant_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "key_id", + "in": "path", + "description": "The unique identifier for the API Key", + "required": true, + "schema": { + "type": "string" + } + } ], - "summary": "PaymentMethods - Create", - "description": "PaymentMethods - Create\n\nTo create a payment method against a customer object. In case of cards, this API could be used only by PCI compliant merchants", - "operationId": "Create a Payment Method", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentMethodCreate" + "$ref": "#/components/schemas/UpdateApiKeyRequest" } } }, @@ -1061,58 +1102,56 @@ }, "responses": { "200": { - "description": "Payment Method Created", + "description": "API Key updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentMethodResponse" + "$ref": "#/components/schemas/RetrieveApiKeyResponse" } } } }, - "400": { - "description": "Invalid Data" + "404": { + "description": "API Key not found" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/payment_methods/{method_id}": { + "/blocklist": { "get": { "tags": [ - "Payment Methods" + "Blocklist" ], - "summary": "Payment Method - Retrieve", - "description": "Payment Method - Retrieve\n\nTo retrieve a payment method", - "operationId": "Retrieve a Payment method", + "operationId": "List Blocked fingerprints of a particular kind", "parameters": [ { - "name": "method_id", - "in": "path", - "description": "The unique identifier for the Payment Method", + "name": "data_kind", + "in": "query", + "description": "Kind of the fingerprint list requested", "required": true, "schema": { - "type": "string" + "$ref": "#/components/schemas/BlocklistDataKind" } } ], "responses": { "200": { - "description": "Payment Method retrieved", + "description": "Blocked Fingerprints", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentMethodResponse" + "$ref": "#/components/schemas/BlocklistResponse" } } } }, - "404": { - "description": "Payment Method does not exist in records" + "400": { + "description": "Invalid Data" } }, "security": [ @@ -1123,27 +1162,14 @@ }, "post": { "tags": [ - "Payment Methods" - ], - "summary": "Payment Method - Update", - "description": "Payment Method - Update\n\nTo update an existing payment method attached to a customer object. This API is useful for use cases such as updating the card number for expired cards to prevent discontinuity in recurring payments", - "operationId": "Update a Payment method", - "parameters": [ - { - "name": "method_id", - "in": "path", - "description": "The unique identifier for the Payment Method", - "required": true, - "schema": { - "type": "string" - } - } + "Blocklist" ], + "operationId": "Block a Fingerprint", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentMethodUpdate" + "$ref": "#/components/schemas/BlocklistRequest" } } }, @@ -1151,17 +1177,17 @@ }, "responses": { "200": { - "description": "Payment Method updated", + "description": "Fingerprint Blocked", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentMethodResponse" + "$ref": "#/components/schemas/BlocklistResponse" } } } }, - "404": { - "description": "Payment Method does not exist in records" + "400": { + "description": "Invalid Data" } }, "security": [ @@ -1172,35 +1198,32 @@ }, "delete": { "tags": [ - "Payment Methods" + "Blocklist" ], - "summary": "Payment Method - Delete", - "description": "Payment Method - Delete\n\nDelete payment method", - "operationId": "Delete a Payment method", - "parameters": [ - { - "name": "method_id", - "in": "path", - "description": "The unique identifier for the Payment Method", - "required": true, - "schema": { - "type": "string" + "operationId": "Unblock a Fingerprint", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistRequest" + } } - } - ], + }, + "required": true + }, "responses": { "200": { - "description": "Payment Method deleted", + "description": "Fingerprint Unblocked", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentMethodDeleteResponse" + "$ref": "#/components/schemas/BlocklistResponse" } } } }, - "404": { - "description": "Payment Method does not exist in records" + "400": { + "description": "Invalid Data" } }, "security": [ @@ -1210,19 +1233,27 @@ ] } }, - "/payments": { + "/customers": { "post": { "tags": [ - "Payments" + "Customers" ], - "summary": "Payments - Create", - "description": "Payments - Create\n\nTo 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", - "operationId": "Create a Payment", + "summary": "Customers - Create", + "description": "Customers - Create\n\nCreates a customer object and stores the customer details to be reused for future payments.\nIncase the customer already exists in the system, this API will respond with the customer details.", + "operationId": "Create a Customer", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsCreateRequest" + "$ref": "#/components/schemas/CustomerRequest" + }, + "examples": { + "Update name and email of a customer": { + "value": { + "email": "guest@example.com", + "name": "John Doe" + } + } } } }, @@ -1230,17 +1261,17 @@ }, "responses": { "200": { - "description": "Payment created", + "description": "Customer Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsResponse" + "$ref": "#/components/schemas/CustomerResponse" } } } }, "400": { - "description": "Missing Mandatory fields" + "description": "Invalid data" } }, "security": [ @@ -1250,149 +1281,145 @@ ] } }, - "/payments/list": { - "get": { + "/customers/list": { + "post": { "tags": [ - "Payments" + "Customers List" ], - "summary": "Payments - List", - "description": "Payments - List\n\nTo list the payments", - "operationId": "List all Payments", - "parameters": [ - { - "name": "customer_id", - "in": "query", - "description": "The identifier for the customer", - "required": true, - "schema": { - "type": "string" + "summary": "Customers - List", + "description": "Customers - List\n\nLists all the customers for a particular merchant id.", + "operationId": "List all Customers for a Merchant", + "responses": { + "200": { + "description": "Customers retrieved", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerResponse" + } + } + } } }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ { - "name": "starting_after", - "in": "query", - "description": "A cursor for use in pagination, fetch the next list after some object", + "api_key": [] + } + ] + } + }, + "/customers/payment_methods": { + "get": { + "tags": [ + "Payment Methods" + ], + "summary": "List payment methods for a Payment", + "description": "List payment methods for a Payment\n\nLists all the applicable payment methods for a particular payment tied to the `client_secret`.", + "operationId": "List all Payment Methods for a Customer", + "parameters": [ + { + "name": "client-secret", + "in": "path", + "description": "A secret known only to your client and the authorization server. Used for client side authentication", "required": true, "schema": { "type": "string" } }, { - "name": "ending_before", - "in": "query", - "description": "A cursor for use in pagination, fetch the previous list before some object", + "name": "customer_id", + "in": "path", + "description": "The unique identifier for the customer account", "required": true, "schema": { "type": "string" } }, { - "name": "limit", + "name": "accepted_country", "in": "query", - "description": "Limit on the number of objects to return", + "description": "The two-letter ISO currency code", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "array", + "items": { + "type": "string" + } } }, { - "name": "created", - "in": "query", - "description": "The time at which payment is created", + "name": "accepted_currency", + "in": "path", + "description": "The three-letter ISO currency code", "required": true, "schema": { - "type": "string", - "format": "date-time" + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + } } }, { - "name": "created_lt", + "name": "minimum_amount", "in": "query", - "description": "Time less than the payment created time", + "description": "The minimum amount accepted for processing by the particular payment method.", "required": true, "schema": { - "type": "string", - "format": "date-time" + "type": "integer", + "format": "int64" } }, { - "name": "created_gt", + "name": "maximum_amount", "in": "query", - "description": "Time greater than the payment created time", + "description": "The maximum amount accepted for processing by the particular payment method.", "required": true, "schema": { - "type": "string", - "format": "date-time" + "type": "integer", + "format": "int64" } }, { - "name": "created_lte", + "name": "recurring_payment_enabled", "in": "query", - "description": "Time less than or equals to the payment created time", + "description": "Indicates whether the payment method is eligible for recurring payments", "required": true, "schema": { - "type": "string", - "format": "date-time" + "type": "boolean" } }, { - "name": "created_gte", + "name": "installment_payment_enabled", "in": "query", - "description": "Time greater than or equals to the payment created time", + "description": "Indicates whether the payment method is eligible for installment payments", "required": true, "schema": { - "type": "string", - "format": "date-time" + "type": "boolean" } } ], "responses": { "200": { - "description": "Received payment list" - }, - "404": { - "description": "No payments found" - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, - "/payments/session_tokens": { - "post": { - "tags": [ - "Payments" - ], - "summary": "Payments - Session token", - "description": "Payments - Session token\n\nTo create the session object or to get session token for wallets", - "operationId": "Create Session tokens for a Payment", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentsSessionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Payment session object created or session token was retrieved from wallets", + "description": "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsSessionResponse" + "$ref": "#/components/schemas/CustomerPaymentMethodsListResponse" } } } }, "400": { - "description": "Missing mandatory fields" + "description": "Invalid Data" + }, + "404": { + "description": "Payment Methods does not exist in records" } }, "security": [ @@ -1402,48 +1429,38 @@ ] } }, - "/payments/{payment_id}": { + "/customers/{customer_id}": { "get": { "tags": [ - "Payments" + "Customers" ], - "summary": "Payments - Retrieve", - "description": "Payments - Retrieve\n\nTo 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", - "operationId": "Retrieve a Payment", + "summary": "Customers - Retrieve", + "description": "Customers - Retrieve\n\nRetrieves a customer's details.", + "operationId": "Retrieve a Customer", "parameters": [ { - "name": "payment_id", + "name": "customer_id", "in": "path", - "description": "The identifier for payment", + "description": "The unique identifier for the Customer", "required": true, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentRetrieveBody" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Gets the payment with final status", + "description": "Customer Retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsResponse" + "$ref": "#/components/schemas/CustomerResponse" } } } }, "404": { - "description": "No payment found" + "description": "Customer was not found" } }, "security": [ @@ -1451,22 +1468,22 @@ "api_key": [] }, { - "publishable_key": [] + "ephemeral_key": [] } ] }, "post": { "tags": [ - "Payments" + "Customers" ], - "summary": "Payments - Update", - "description": "Payments - Update\n\nTo update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created", - "operationId": "Update a Payment", + "summary": "Customers - Update", + "description": "Customers - Update\n\nUpdates the customer's details in a customer object.", + "operationId": "Update a Customer", "parameters": [ { - "name": "payment_id", + "name": "customer_id", "in": "path", - "description": "The identifier for payment", + "description": "The unique identifier for the Customer", "required": true, "schema": { "type": "string" @@ -1477,7 +1494,15 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsRequest" + "$ref": "#/components/schemas/CustomerRequest" + }, + "examples": { + "Update name and email of a customer": { + "value": { + "email": "guest@example.com", + "name": "John Doe" + } + } } } }, @@ -1485,64 +1510,17 @@ }, "responses": { "200": { - "description": "Payment updated", + "description": "Customer was Updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsResponse" + "$ref": "#/components/schemas/CustomerResponse" } } } }, - "400": { - "description": "Missing mandatory fields" - } - }, - "security": [ - { - "api_key": [] - }, - { - "publishable_key": [] - } - ] - } - }, - "/payments/{payment_id}/cancel": { - "post": { - "tags": [ - "Payments" - ], - "summary": "Payments - Cancel", - "description": "Payments - Cancel\n\nA Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action", - "operationId": "Cancel a Payment", - "parameters": [ - { - "name": "payment_id", - "in": "path", - "description": "The identifier for payment", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentsCancelRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Payment canceled" - }, - "400": { - "description": "Missing mandatory fields" + "404": { + "description": "Customer was not found" } }, "security": [ @@ -1550,50 +1528,38 @@ "api_key": [] } ] - } - }, - "/payments/{payment_id}/capture": { - "post": { + }, + "delete": { "tags": [ - "Payments" + "Customers" ], - "summary": "Payments - Capture", - "description": "Payments - Capture\n\nTo capture the funds for an uncaptured payment", - "operationId": "Capture a Payment", + "summary": "Customers - Delete", + "description": "Customers - Delete\n\nDelete a customer record.", + "operationId": "Delete a Customer", "parameters": [ { - "name": "payment_id", + "name": "customer_id", "in": "path", - "description": "The identifier for payment", + "description": "The unique identifier for the Customer", "required": true, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentsCaptureRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Payment captured", + "description": "Customer was Deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsResponse" + "$ref": "#/components/schemas/CustomerDeleteResponse" } } } }, - "400": { - "description": "Missing mandatory fields" + "404": { + "description": "Customer was not found" } }, "security": [ @@ -1603,132 +1569,103 @@ ] } }, - "/payments/{payment_id}/confirm": { - "post": { + "/customers/{customer_id}/payment_methods": { + "get": { "tags": [ - "Payments" + "Payment Methods" ], - "summary": "Payments - Confirm", - "description": "Payments - Confirm\n\nThis 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", - "operationId": "Confirm a Payment", + "summary": "List payment methods for a Customer", + "description": "List payment methods for a Customer\n\nLists all the applicable payment methods for a particular Customer ID.", + "operationId": "List all Payment Methods for a Customer", "parameters": [ { - "name": "payment_id", + "name": "customer_id", "in": "path", - "description": "The identifier for payment", + "description": "The unique identifier for the customer account", "required": true, "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentsRequest" + }, + { + "name": "accepted_country", + "in": "query", + "description": "The two-letter ISO currency code", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" } } }, - "required": true - }, - "responses": { - "200": { - "description": "Payment confirmed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentsResponse" - } + { + "name": "accepted_currency", + "in": "path", + "description": "The three-letter ISO currency code", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" } } }, - "400": { - "description": "Missing mandatory fields" - } - }, - "security": [ { - "api_key": [] + "name": "minimum_amount", + "in": "query", + "description": "The minimum amount accepted for processing by the particular payment method.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } }, { - "publishable_key": [] - } - ] - } - }, - "/payouts/create": { - "post": { - "tags": [ - "Payouts" - ], - "summary": "Payouts - Create", - "description": "Payouts - Create", - "operationId": "Create a Payout", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayoutCreateRequest" - } + "name": "maximum_amount", + "in": "query", + "description": "The maximum amount accepted for processing by the particular payment method.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" } }, - "required": true - }, - "responses": { - "200": { - "description": "Payout created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayoutCreateResponse" - } - } + { + "name": "recurring_payment_enabled", + "in": "query", + "description": "Indicates whether the payment method is eligible for recurring payments", + "required": true, + "schema": { + "type": "boolean" } }, - "400": { - "description": "Missing Mandatory fields" - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, - "/payouts/{payout_id}": { - "get": { - "tags": [ - "Payouts" - ], - "summary": "Payouts - Retrieve", - "description": "Payouts - Retrieve", - "operationId": "Retrieve a Payout", - "parameters": [ { - "name": "payout_id", - "in": "path", - "description": "The identifier for payout]", + "name": "installment_payment_enabled", + "in": "query", + "description": "Indicates whether the payment method is eligible for installment payments", "required": true, "schema": { - "type": "string" + "type": "boolean" } } ], "responses": { "200": { - "description": "Payout retrieved", + "description": "Payment Methods retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutCreateResponse" + "$ref": "#/components/schemas/CustomerPaymentMethodsListResponse" } } } }, + "400": { + "description": "Invalid Data" + }, "404": { - "description": "Payout does not exist in our records" + "description": "Payment Methods does not exist in records" } }, "security": [ @@ -1736,48 +1673,148 @@ "api_key": [] } ] - }, - "post": { + } + }, + "/disputes/list": { + "get": { "tags": [ - "Payouts" + "Disputes" ], - "summary": "Payouts - Update", - "description": "Payouts - Update", - "operationId": "Update a Payout", + "summary": "Disputes - List Disputes", + "description": "Disputes - List Disputes\nLists all the Disputes for a merchant", + "operationId": "List Disputes", "parameters": [ { - "name": "payout_id", - "in": "path", - "description": "The identifier for payout]", - "required": true, + "name": "limit", + "in": "query", + "description": "The maximum number of Dispute Objects to include in the response", + "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64", + "nullable": true } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayoutCreateRequest" - } + }, + { + "name": "dispute_status", + "in": "query", + "description": "The status of dispute", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/DisputeStatus" + } + ], + "nullable": true } }, - "required": true - }, + { + "name": "dispute_stage", + "in": "query", + "description": "The stage of dispute", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/DisputeStage" + } + ], + "nullable": true + } + }, + { + "name": "reason", + "in": "query", + "description": "The reason for dispute", + "required": false, + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "connector", + "in": "query", + "description": "The connector linked to dispute", + "required": false, + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "received_time", + "in": "query", + "description": "The time at which dispute is received", + "required": false, + "schema": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + { + "name": "received_time.lt", + "in": "query", + "description": "Time less than the dispute received time", + "required": false, + "schema": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + { + "name": "received_time.gt", + "in": "query", + "description": "Time greater than the dispute received time", + "required": false, + "schema": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + { + "name": "received_time.lte", + "in": "query", + "description": "Time less than or equals to the dispute received time", + "required": false, + "schema": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + { + "name": "received_time.gte", + "in": "query", + "description": "Time greater than or equals to the dispute received time", + "required": false, + "schema": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + ], "responses": { "200": { - "description": "Payout updated", + "description": "The dispute list was retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutCreateResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/DisputeResponse" + } } } } }, - "400": { - "description": "Missing Mandatory fields" + "401": { + "description": "Unauthorized request" } }, "security": [ @@ -1787,30 +1824,60 @@ ] } }, - "/payouts/{payout_id}/cancel": { - "post": { + "/disputes/{dispute_id}": { + "get": { "tags": [ - "Payouts" + "Disputes" ], - "summary": "Payouts - Cancel", - "description": "Payouts - Cancel", - "operationId": "Cancel a Payout", + "summary": "Disputes - Retrieve Dispute", + "description": "Disputes - Retrieve Dispute\nRetrieves a dispute", + "operationId": "Retrieve a Dispute", "parameters": [ { - "name": "payout_id", + "name": "dispute_id", "in": "path", - "description": "The identifier for payout", + "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\nCreates a GSM (Global Status Mapping) Rule. A GSM rule is used to map a connector's error message/error code combination during a particular payments flow/sub-flow to Hyperswitch's unified status/error code/error message combination. It is also used to decide the next action in the flow - retry/requeue/do_default", + "operationId": "Create Gsm Rule", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutActionRequest" + "$ref": "#/components/schemas/GsmCreateRequest" } } }, @@ -1818,11 +1885,11 @@ }, "responses": { "200": { - "description": "Payout cancelled", + "description": "Gsm created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutCreateResponse" + "$ref": "#/components/schemas/GsmResponse" } } } @@ -1833,35 +1900,24 @@ }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/payouts/{payout_id}/fulfill": { + "/gsm/delete": { "post": { "tags": [ - "Payouts" - ], - "summary": "Payouts - Fulfill", - "description": "Payouts - Fulfill", - "operationId": "Fulfill a Payout", - "parameters": [ - { - "name": "payout_id", - "in": "path", - "description": "The identifier for payout", - "required": true, - "schema": { - "type": "string" - } - } + "Gsm" ], + "summary": "Gsm - Delete", + "description": "Gsm - Delete\n\nDeletes a Gsm Rule", + "operationId": "Delete Gsm Rule", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutActionRequest" + "$ref": "#/components/schemas/GsmDeleteRequest" } } }, @@ -1869,11 +1925,11 @@ }, "responses": { "200": { - "description": "Payout fulfilled", + "description": "Gsm deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutCreateResponse" + "$ref": "#/components/schemas/GsmDeleteResponse" } } } @@ -1884,24 +1940,24 @@ }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/refunds": { + "/gsm/get": { "post": { "tags": [ - "Refunds" + "Gsm" ], - "summary": "Refunds - Create", - "description": "Refunds - Create\n\nTo create a refund against an already processed payment", - "operationId": "Create a Refund", + "summary": "Gsm - Get", + "description": "Gsm - Get\n\nRetrieves a Gsm Rule", + "operationId": "Retrieve Gsm Rule", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundRequest" + "$ref": "#/components/schemas/GsmRetrieveRequest" } } }, @@ -1909,11 +1965,11 @@ }, "responses": { "200": { - "description": "Refund created", + "description": "Gsm retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundResponse" + "$ref": "#/components/schemas/GsmResponse" } } } @@ -1924,24 +1980,24 @@ }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/refunds/list": { + "/gsm/update": { "post": { "tags": [ - "Refunds" + "Gsm" ], - "summary": "Refunds - List", - "description": "Refunds - List\n\nTo list the refunds associated with a payment_id or with the merchant, if payment_id is not provided", - "operationId": "List all Refunds", + "summary": "Gsm - Update", + "description": "Gsm - Update\n\nUpdates a Gsm Rule", + "operationId": "Update Gsm Rule", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundListRequest" + "$ref": "#/components/schemas/GsmUpdateRequest" } } }, @@ -1949,36 +2005,39 @@ }, "responses": { "200": { - "description": "List of refunds", + "description": "Gsm updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundListResponse" + "$ref": "#/components/schemas/GsmResponse" } } } + }, + "400": { + "description": "Missing Mandatory fields" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/refunds/{refund_id}": { - "get": { + "/mandates/revoke/{mandate_id}": { + "post": { "tags": [ - "Refunds" + "Mandates" ], - "summary": "Refunds - Retrieve (GET)", - "description": "Refunds - Retrieve (GET)\n\nTo 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", - "operationId": "Retrieve a Refund", + "summary": "Mandates - Revoke Mandate", + "description": "Mandates - Revoke Mandate\n\nRevokes a mandate created using the Payments/Create API", + "operationId": "Revoke a Mandate", "parameters": [ { - "name": "refund_id", + "name": "mandate_id", "in": "path", - "description": "The identifier for refund", + "description": "The identifier for a mandate", "required": true, "schema": { "type": "string" @@ -1987,17 +2046,17 @@ ], "responses": { "200": { - "description": "Refund retrieved", + "description": "The mandate was revoked successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundResponse" + "$ref": "#/components/schemas/MandateRevokedResponse" } } } }, - "404": { - "description": "Refund does not exist in our records" + "400": { + "description": "Mandate does not exist in our records" } }, "security": [ @@ -2005,19 +2064,62 @@ "api_key": [] } ] - }, - "post": { + } + }, + "/mandates/{mandate_id}": { + "get": { "tags": [ - "Refunds" + "Mandates" ], - "summary": "Refunds - Update", - "description": "Refunds - Update\n\nTo update the properties of a Refund object. This may include attaching a reason for the refund or metadata fields", - "operationId": "Update a Refund", + "summary": "Mandates - Retrieve Mandate", + "description": "Mandates - Retrieve Mandate\n\nRetrieves a mandate created using the Payments/Create API", + "operationId": "Retrieve a Mandate", "parameters": [ { - "name": "refund_id", + "name": "mandate_id", "in": "path", - "description": "The identifier for refund", + "description": "The identifier for mandate", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The mandate was retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MandateResponse" + } + } + } + }, + "404": { + "description": "Mandate does not exist in our records" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/payment_link/{payment_link_id}": { + "get": { + "tags": [ + "Payments" + ], + "summary": "Payments Link - Retrieve", + "description": "Payments Link - Retrieve\n\nTo retrieve the properties of a Payment Link. This may be used to get the status of a previously initiated payment or next action for an ongoing payment", + "operationId": "Retrieve a Payment Link", + "parameters": [ + { + "name": "payment_link_id", + "in": "path", + "description": "The identifier for payment link", "required": true, "schema": { "type": "string" @@ -2028,7 +2130,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundUpdateRequest" + "$ref": "#/components/schemas/RetrievePaymentLinkRequest" } } }, @@ -2036,2262 +2138,4702 @@ }, "responses": { "200": { - "description": "Refund updated", + "description": "Gets details regarding payment link", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundResponse" + "$ref": "#/components/schemas/RetrievePaymentLinkResponse" } } } }, - "400": { - "description": "Missing Mandatory fields" + "404": { + "description": "No payment link found" } }, "security": [ { "api_key": [] + }, + { + "publishable_key": [] } ] } - } - }, - "components": { - "schemas": { - "AcceptanceType": { - "type": "string", - "enum": [ - "online", - "offline" - ] - }, - "AcceptedCountries": { - "oneOf": [ - { - "type": "object", - "required": [ - "type", - "list" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "enable_only" - ] + }, + "/payment_methods": { + "post": { + "tags": [ + "Payment Methods" + ], + "summary": "PaymentMethods - Create", + "description": "PaymentMethods - Create\n\nCreates and stores a payment method against a customer.\nIn case of cards, this API should be used only by PCI compliant merchants.", + "operationId": "Create a Payment Method", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodCreate" }, - "list": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CountryAlpha2" + "examples": { + "Save a card": { + "value": { + "card": { + "card_exp_month": "11", + "card_exp_year": "25", + "card_holder_name": "John Doe", + "card_number": "4242424242424242" + }, + "customer_id": "{{customer_id}}", + "payment_method": "card", + "payment_method_issuer": "Visa", + "payment_method_type": "credit" + } } } } }, - { - "type": "object", - "required": [ - "type", - "list" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "disable_only" - ] - }, - "list": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CountryAlpha2" + "required": true + }, + "responses": { + "200": { + "description": "Payment Method Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodResponse" } } } }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "all_accepted" - ] - } - } + "api_key": [] } + ] + } + }, + "/payment_methods/{method_id}": { + "get": { + "tags": [ + "Payment Methods" ], - "discriminator": { - "propertyName": "type" - } - }, - "AcceptedCurrencies": { - "oneOf": [ + "summary": "Payment Method - Retrieve", + "description": "Payment Method - Retrieve\n\nRetrieves a payment method of a customer.", + "operationId": "Retrieve a Payment method", + "parameters": [ { - "type": "object", - "required": [ - "type", - "list" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "enable_only" - ] - }, - "list": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" - } - } + "name": "method_id", + "in": "path", + "description": "The unique identifier for the Payment Method", + "required": true, + "schema": { + "type": "string" } - }, - { - "type": "object", - "required": [ - "type", - "list" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "disable_only" - ] - }, - "list": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" + } + ], + "responses": { + "200": { + "description": "Payment Method retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodResponse" } } } }, + "404": { + "description": "Payment Method does not exist in records" + } + }, + "security": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "all_accepted" - ] - } - } + "api_key": [] } - ], - "discriminator": { - "propertyName": "type" - } + ] }, - "AchBankTransfer": { - "type": "object", - "required": [ - "bank_name", - "bank_country_code", - "bank_city", - "bank_account_number", - "bank_routing_number" + "post": { + "tags": [ + "Payment Methods" ], - "properties": { - "bank_name": { - "type": "string", - "description": "Bank name", - "example": "Deutsche Bank" - }, - "bank_country_code": { - "$ref": "#/components/schemas/CountryAlpha2" - }, - "bank_city": { - "type": "string", - "description": "Bank city", - "example": "California" + "summary": "Payment Method - Update", + "description": "Payment Method - Update\n\nUpdate an existing payment method of a customer.\nThis API is useful for use cases such as updating the card number for expired cards to prevent discontinuity in recurring payments.", + "operationId": "Update a Payment method", + "parameters": [ + { + "name": "method_id", + "in": "path", + "description": "The unique identifier for the Payment Method", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodUpdate" + } + } }, - "bank_account_number": { - "type": "string", - "description": "Bank account number is an unique identifier assigned by a bank to a customer.", - "example": "000123456" + "required": true + }, + "responses": { + "200": { + "description": "Payment Method updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodResponse" + } + } + } }, - "bank_routing_number": { - "type": "string", - "description": "[9 digits] Routing number - used in USA for identifying a specific bank.", - "example": "110000000" + "404": { + "description": "Payment Method does not exist in records" } - } + }, + "security": [ + { + "api_key": [] + } + ] }, - "AchBillingDetails": { - "type": "object", - "required": [ - "email" + "delete": { + "tags": [ + "Payment Methods" ], - "properties": { - "email": { - "type": "string", - "description": "The Email ID for ACH billing", - "example": "example@me.com" + "summary": "Payment Method - Delete", + "description": "Payment Method - Delete\n\nDeletes a payment method of a customer.", + "operationId": "Delete a Payment method", + "parameters": [ + { + "name": "method_id", + "in": "path", + "description": "The unique identifier for the Payment Method", + "required": true, + "schema": { + "type": "string" + } } - } - }, - "AchTransfer": { - "type": "object", - "required": [ - "account_number", - "bank_name", - "routing_number", - "swift_code" ], - "properties": { - "account_number": { - "type": "string", - "example": "122385736258" - }, - "bank_name": { - "type": "string" - }, - "routing_number": { - "type": "string", - "example": "012" + "responses": { + "200": { + "description": "Payment Method deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodDeleteResponse" + } + } + } }, - "swift_code": { - "type": "string", - "example": "234" + "404": { + "description": "Payment Method does not exist in records" } - } - }, - "Address": { - "type": "object", - "properties": { - "address": { - "allOf": [ - { - "$ref": "#/components/schemas/AddressDetails" + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/payments": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Payments - Create", + "description": "Payments - Create\n\n**Creates a payment object when amount and currency are passed.** This API is also used to create a mandate by passing the `mandate_object`.\n\nTo completely process a payment you will have to create a payment, attach a payment method, confirm and capture funds.\n\nDepending on the user journey you wish to achieve, you may opt to complete all the steps in a single request by attaching a payment method, setting `confirm=true` and `capture_method = automatic` in the *Payments/Create API* request or you could use the following sequence of API requests to achieve the same:\n\n1. Payments - Create\n\n2. Payments - Update\n\n3. Payments - Confirm\n\n4. Payments - Capture.\n\nUse the client secret returned in this API along with your publishable key to make subsequent API calls from your client", + "operationId": "Create a Payment", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsCreateRequest" + }, + "examples": { + "Create a 3DS payment": { + "value": { + "amount": 6540, + "authentication_type": "three_ds", + "currency": "USD" + } + }, + "Create a manual capture payment": { + "value": { + "amount": 6540, + "billing": { + "address": { + "city": "San Fransico", + "country": "US", + "first_name": "joseph", + "last_name": "Doe", + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "state": "California", + "zip": "94122" + }, + "phone": { + "country_code": "+91", + "number": "8056594427" + } + }, + "currency": "USD", + "customer": { + "id": "cus_abcdefgh" + } + } + }, + "Create a payment and save the card": { + "value": { + "amount": 6540, + "authentication_type": "no_three_ds", + "confirm": true, + "currency": "USD", + "customer_id": "StripeCustomer123", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_cvc": "123", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_number": "4242424242424242" + } + }, + "setup_future_usage": "off_session" + } + }, + "Create a payment using an already saved card's token": { + "value": { + "amount": 6540, + "card_cvc": "123", + "client_secret": "{{client_secret}}", + "confirm": true, + "currency": "USD", + "payment_method": "card", + "payment_token": "{{payment_token}}" + } + }, + "Create a payment with customer details and metadata": { + "value": { + "amount": 6540, + "currency": "USD", + "customer": { + "email": "john@example.com", + "id": "cus_abcdefgh", + "name": "John Dough", + "phone": "9999999999" + }, + "description": "Its my first payment request", + "metadata": { + "udf1": "some-value", + "udf2": "some-value" + }, + "payment_id": "abcdefghijklmnopqrstuvwxyz", + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS" + } + }, + "Create a payment with minimal fields": { + "value": { + "amount": 6540, + "currency": "USD" + } + }, + "Create a recurring payment with mandate_id": { + "value": { + "amount": 6540, + "authentication_type": "no_three_ds", + "confirm": true, + "currency": "USD", + "customer_id": "StripeCustomer", + "mandate_id": "{{mandate_id}}", + "off_session": true + } + }, + "Create a setup mandate payment": { + "value": { + "amount": 6540, + "authentication_type": "no_three_ds", + "confirm": true, + "currency": "USD", + "customer_id": "StripeCustomer123", + "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": 6540, + "currency": "USD" + } + } + }, + "payment_method": "card", + "payment_method_data": { + "card": { + "card_cvc": "123", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_number": "4242424242424242" + } + }, + "setup_future_usage": "off_session" + } + } } - ], - "nullable": true + } }, - "phone": { - "allOf": [ - { - "$ref": "#/components/schemas/PhoneDetails" + "required": true + }, + "responses": { + "200": { + "description": "Payment created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } } - ], - "nullable": true - } - } - }, - "AddressDetails": { - "type": "object", - "properties": { - "city": { - "type": "string", - "description": "The address city", - "example": "New York", - "nullable": true, - "maxLength": 50 + } }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/CountryAlpha2" - } - ], - "nullable": true + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/payments/list": { + "get": { + "tags": [ + "Payments" + ], + "summary": "Payments - List", + "description": "Payments - List\n\nTo list the *payments*", + "operationId": "List all Payments", + "parameters": [ + { + "name": "customer_id", + "in": "query", + "description": "The identifier for the customer", + "required": true, + "schema": { + "type": "string" + } }, - "line1": { - "type": "string", - "description": "The first line of the address", - "example": "123, King Street", - "nullable": true, - "maxLength": 200 + { + "name": "starting_after", + "in": "query", + "description": "A cursor for use in pagination, fetch the next list after some object", + "required": true, + "schema": { + "type": "string" + } }, - "line2": { - "type": "string", - "description": "The second line of the address", - "example": "Powelson Avenue", - "nullable": true, - "maxLength": 50 - }, - "line3": { - "type": "string", - "description": "The third line of the address", - "example": "Bridgewater", - "nullable": true, - "maxLength": 50 + { + "name": "ending_before", + "in": "query", + "description": "A cursor for use in pagination, fetch the previous list before some object", + "required": true, + "schema": { + "type": "string" + } }, - "zip": { - "type": "string", - "description": "The zip/postal code for the address", - "example": "08807", - "nullable": true, - "maxLength": 50 + { + "name": "limit", + "in": "query", + "description": "Limit on the number of objects to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } }, - "state": { - "type": "string", - "description": "The address state", - "example": "New York", - "nullable": true + { + "name": "created", + "in": "query", + "description": "The time at which payment is created", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } }, - "first_name": { - "type": "string", - "description": "The first name for the address", - "example": "John", - "nullable": true, - "maxLength": 255 + { + "name": "created_lt", + "in": "query", + "description": "Time less than the payment created time", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } }, - "last_name": { - "type": "string", - "description": "The last name for the address", - "example": "Doe", - "nullable": true, - "maxLength": 255 - } - } - }, - "AirwallexData": { - "type": "object", - "properties": { - "payload": { - "type": "string", - "description": "payload required by airwallex", - "nullable": true - } - } - }, - "AlfamartVoucherData": { - "type": "object", - "required": [ - "first_name", - "last_name", - "email" - ], - "properties": { - "first_name": { - "type": "string", - "description": "The billing first name for Alfamart", - "example": "Jane" + { + "name": "created_gt", + "in": "query", + "description": "Time greater than the payment created time", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } }, - "last_name": { - "type": "string", - "description": "The billing second name for Alfamart", - "example": "Doe" + { + "name": "created_lte", + "in": "query", + "description": "Time less than or equals to the payment created time", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } }, - "email": { - "type": "string", - "description": "The Email ID for Alfamart", - "example": "example@me.com" + { + "name": "created_gte", + "in": "query", + "description": "Time greater than or equals to the payment created time", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } } - } - }, - "AliPayHkRedirection": { - "type": "object" - }, - "AliPayQr": { - "type": "object" - }, - "AliPayRedirection": { - "type": "object" - }, - "AmountInfo": { - "type": "object", - "required": [ - "label", - "amount" ], - "properties": { - "label": { - "type": "string", - "description": "The label must be the name of the merchant." - }, - "type": { - "type": "string", - "description": "A value that indicates whether the line item(Ex: total, tax, discount, or grand total) is final or pending.", - "nullable": true + "responses": { + "200": { + "description": "Successfully retrieved a payment list", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentListResponse" + } + } + } + } }, - "amount": { - "type": "string", - "description": "The total amount for the payment" + "404": { + "description": "No payments found" } - } - }, - "ApiKeyExpiration": { - "oneOf": [ - { - "type": "string", - "enum": [ - "never" - ] - }, + }, + "security": [ { - "type": "string", - "format": "date-time" + "api_key": [] } ] - }, - "ApplePayPaymentRequest": { - "type": "object", - "required": [ - "country_code", - "currency_code", - "total" + } + }, + "/payments/session_tokens": { + "post": { + "tags": [ + "Payments" ], - "properties": { - "country_code": { - "$ref": "#/components/schemas/CountryAlpha2" - }, - "currency_code": { - "$ref": "#/components/schemas/Currency" - }, - "total": { - "$ref": "#/components/schemas/AmountInfo" - }, - "merchant_capabilities": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The list of merchant capabilities(ex: whether capable of 3ds or no-3ds)", - "nullable": true + "summary": "Payments - Session token", + "description": "Payments - Session token\n\nCreates a session object or a session token for wallets like Apple Pay, Google Pay, etc. These tokens are used by Hyperswitch's SDK to initiate these wallets' SDK.", + "operationId": "Create Session tokens for a Payment", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsSessionRequest" + } + } }, - "supported_networks": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The list of supported networks", - "nullable": true + "required": true + }, + "responses": { + "200": { + "description": "Payment session object created or session token was retrieved from wallets", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsSessionResponse" + } + } + } }, - "merchant_identifier": { - "type": "string", - "nullable": true + "400": { + "description": "Missing mandatory fields" } - } - }, - "ApplePayRedirectData": { - "type": "object" - }, - "ApplePaySessionResponse": { - "oneOf": [ + }, + "security": [ { - "$ref": "#/components/schemas/ThirdPartySdkSessionResponse" + "publishable_key": [] + } + ] + } + }, + "/payments/{payment_id}": { + "get": { + "tags": [ + "Payments" + ], + "summary": "Payments - Retrieve", + "description": "Payments - Retrieve\n\nRetrieves a Payment. This API can also be used to get the status of a previously initiated payment or next action for an ongoing payment", + "operationId": "Retrieve a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentRetrieveBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Gets the payment with final status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } }, + "404": { + "description": "No payment found" + } + }, + "security": [ { - "$ref": "#/components/schemas/NoThirdPartySdkSessionResponse" + "api_key": [] }, { - "type": "object", - "default": null, - "nullable": true + "publishable_key": [] } ] }, - "ApplePayThirdPartySdkData": { - "type": "object" - }, - "ApplePayWalletData": { - "type": "object", - "required": [ - "payment_data", - "payment_method", - "transaction_identifier" + "post": { + "tags": [ + "Payments" ], - "properties": { - "payment_data": { - "type": "string", - "description": "The payment data of Apple pay" + "summary": "Payments - Update", + "description": "Payments - Update\n\nTo update the properties of a *PaymentIntent* object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created", + "operationId": "Update a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsUpdateRequest" + }, + "examples": { + "Update the payment amount": { + "value": { + "amount": 7654 + } + }, + "Update the shipping address": { + "value": { + "shipping": { + "address": { + "city": "San Fransico", + "country": "US", + "first_name": "joseph", + "last_name": "Doe", + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "state": "California", + "zip": "94122" + }, + "phone": { + "country_code": "+91", + "number": "8056594427" + } + } + } + } + } + } }, - "payment_method": { - "$ref": "#/components/schemas/ApplepayPaymentMethod" + "required": true + }, + "responses": { + "200": { + "description": "Payment updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } }, - "transaction_identifier": { - "type": "string", - "description": "The unique identifier for the transaction" + "400": { + "description": "Missing mandatory fields" } - } - }, - "ApplepayConnectorMetadataRequest": { - "type": "object", - "properties": { - "session_token_data": { - "allOf": [ - { - "$ref": "#/components/schemas/SessionTokenInfo" - } - ], - "nullable": true + }, + "security": [ + { + "api_key": [] + }, + { + "publishable_key": [] } - } - }, - "ApplepayPaymentMethod": { - "type": "object", - "required": [ - "display_name", - "network", - "type" + ] + } + }, + "/payments/{payment_id}/cancel": { + "post": { + "tags": [ + "Payments" ], - "properties": { - "display_name": { - "type": "string", - "description": "The name to be displayed on Apple Pay button" + "summary": "Payments - Cancel", + "description": "Payments - Cancel\n\nA Payment could can be cancelled when it is in one of these statuses: `requires_payment_method`, `requires_capture`, `requires_confirmation`, `requires_customer_action`.", + "operationId": "Cancel a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsCancelRequest" + }, + "examples": { + "Cancel the payment with cancellation reason": { + "value": { + "cancellation_reason": "requested_by_customer" + } + }, + "Cancel the payment with minimal fields": { + "value": {} + } + } + } }, - "network": { - "type": "string", - "description": "The network of the Apple pay payment method" + "required": true + }, + "responses": { + "200": { + "description": "Payment canceled" }, - "type": { - "type": "string", - "description": "The type of the payment method" + "400": { + "description": "Missing mandatory fields" } - } - }, - "ApplepaySessionTokenResponse": { - "type": "object", - "required": [ - "session_token_data", - "connector", - "delayed_session_token", - "sdk_next_action" + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/payments/{payment_id}/capture": { + "post": { + "tags": [ + "Payments" ], - "properties": { - "session_token_data": { - "$ref": "#/components/schemas/ApplePaySessionResponse" + "summary": "Payments - Capture", + "description": "Payments - Capture\n\nTo capture the funds for an uncaptured payment", + "operationId": "Capture a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsCaptureRequest" + }, + "examples": { + "Capture partial amount": { + "value": { + "amount_to_capture": 654 + } + }, + "Capture the full amount": { + "value": {} + } + } + } }, - "payment_request_data": { - "allOf": [ - { - "$ref": "#/components/schemas/ApplePayPaymentRequest" + "required": true + }, + "responses": { + "200": { + "description": "Payment captured", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } } - ], - "nullable": true + } }, - "connector": { - "type": "string", - "description": "The session token is w.r.t this connector" - }, - "delayed_session_token": { - "type": "boolean", - "description": "Identifier for the delayed session response" - }, - "sdk_next_action": { - "$ref": "#/components/schemas/SdkNextAction" - }, - "connector_reference_id": { - "type": "string", - "description": "The connector transaction id", - "nullable": true - }, - "connector_sdk_public_key": { - "type": "string", - "description": "The public key id is to invoke third party sdk", - "nullable": true - }, - "connector_merchant_id": { - "type": "string", - "description": "The connector merchant id", - "nullable": true + "400": { + "description": "Missing mandatory fields" + } + }, + "security": [ + { + "api_key": [] } - } - }, - "AttemptStatus": { - "type": "string", - "enum": [ - "started", - "authentication_failed", - "router_declined", - "authentication_pending", - "authentication_successful", - "authorized", - "authorization_failed", - "charged", - "authorizing", - "cod_initiated", - "voided", - "void_initiated", - "capture_initiated", - "capture_failed", - "void_failed", - "auto_refunded", - "partial_charged", - "unresolved", - "pending", - "failure", - "payment_method_awaited", - "confirmation_awaited", - "device_data_collection_pending" - ] - }, - "AuthenticationType": { - "type": "string", - "enum": [ - "three_ds", - "no_three_ds" ] - }, - "BacsBankTransfer": { - "type": "object", - "required": [ - "bank_name", - "bank_country_code", - "bank_city", - "bank_account_number", - "bank_sort_code" + } + }, + "/payments/{payment_id}/confirm": { + "post": { + "tags": [ + "Payments" ], - "properties": { - "bank_name": { - "type": "string", - "description": "Bank name", - "example": "Deutsche Bank" - }, - "bank_country_code": { - "$ref": "#/components/schemas/CountryAlpha2" - }, - "bank_city": { - "type": "string", - "description": "Bank city", - "example": "California" - }, - "bank_account_number": { - "type": "string", - "description": "Bank account number is an unique identifier assigned by a bank to a customer.", - "example": "000123456" - }, - "bank_sort_code": { - "type": "string", - "description": "[6 digits] Sort Code - used in UK and Ireland for identifying a bank and it's branches.", - "example": "98-76-54" + "summary": "Payments - Confirm", + "description": "Payments - Confirm\n\n**Use this API to confirm the payment and forward the payment to the payment processor.**\n\nAlternatively you can confirm the payment within the *Payments/Create* API by setting `confirm=true`. After confirmation, the payment could either:\n\n1. fail with `failed` status or\n\n2. transition to a `requires_customer_action` status with a `next_action` block or\n\n3. succeed with either `succeeded` in case of automatic capture or `requires_capture` in case of manual capture", + "operationId": "Confirm a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } } - } - }, - "BacsBankTransferInstructions": { - "type": "object", - "required": [ - "account_holder_name", - "account_number", - "sort_code" ], - "properties": { - "account_holder_name": { - "type": "string", - "example": "Jane Doe" + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsConfirmRequest" + }, + "examples": { + "Confirm a payment with payment method data": { + "value": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_cvc": "123", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_number": "4242424242424242" + } + }, + "payment_method_type": "credit" + } + } + } + } }, - "account_number": { - "type": "string", - "example": "10244123908" + "required": true + }, + "responses": { + "200": { + "description": "Payment confirmed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } }, - "sort_code": { - "type": "string", - "example": "012" + "400": { + "description": "Missing mandatory fields" } - } - }, - "Bank": { - "oneOf": [ - { - "$ref": "#/components/schemas/AchBankTransfer" - }, + }, + "security": [ { - "$ref": "#/components/schemas/BacsBankTransfer" + "api_key": [] }, { - "$ref": "#/components/schemas/SepaBankTransfer" + "publishable_key": [] } ] - }, - "BankDebitBilling": { - "type": "object", - "required": [ - "name", - "email" + } + }, + "/payments/{payment_id}/incremental_authorization": { + "post": { + "tags": [ + "Payments" ], - "properties": { - "name": { - "type": "string", - "description": "The billing name for bank debits", - "example": "John Doe" - }, - "email": { - "type": "string", - "description": "The billing email for bank debits", - "example": "example@example.com" + "summary": "Payments - Incremental Authorization", + "description": "Payments - Incremental Authorization\n\nAuthorized amount for a payment can be incremented if it is in status: requires_capture", + "operationId": "Increment authorized amount for a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsIncrementalAuthorizationRequest" + } + } }, - "address": { - "allOf": [ - { - "$ref": "#/components/schemas/AddressDetails" + "required": true + }, + "responses": { + "200": { + "description": "Payment authorized amount incremented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } } - ], - "nullable": true + } + }, + "400": { + "description": "Missing mandatory fields" } - } - }, - "BankDebitData": { - "oneOf": [ + }, + "security": [ { - "type": "object", - "required": [ - "ach_bank_debit" - ], - "properties": { - "ach_bank_debit": { - "type": "object", - "description": "Payment Method data for Ach bank debit", - "required": [ - "billing_details", - "account_number", - "routing_number", - "card_holder_name", - "bank_account_holder_name", - "bank_name", - "bank_type", - "bank_holder_type" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/BankDebitBilling" - }, - "account_number": { - "type": "string", - "description": "Account number for ach bank debit payment", - "example": "000123456789" - }, - "routing_number": { - "type": "string", - "description": "Routing number for ach bank debit payment", - "example": "110000000" - }, - "card_holder_name": { - "type": "string", - "example": "John Test" - }, - "bank_account_holder_name": { - "type": "string", - "example": "John Doe" - }, - "bank_name": { - "type": "string", - "example": "ACH" - }, - "bank_type": { - "type": "string", - "example": "Checking" - }, - "bank_holder_type": { - "type": "string", - "example": "Personal" - } - } + "api_key": [] + } + ] + } + }, + "/payouts/create": { + "post": { + "tags": [ + "Payouts" + ], + "summary": "Payouts - Create", + "description": "Payouts - Create", + "operationId": "Create a Payout", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateRequest" } } }, - { - "type": "object", - "required": [ - "sepa_bank_debit" - ], - "properties": { - "sepa_bank_debit": { - "type": "object", - "required": [ - "billing_details", - "iban", - "bank_account_holder_name" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/BankDebitBilling" - }, - "iban": { - "type": "string", - "description": "International bank account number (iban) for SEPA", - "example": "DE89370400440532013000" - }, - "bank_account_holder_name": { - "type": "string", - "description": "Owner name for bank debit", - "example": "A. Schneider" - } + "required": true + }, + "responses": { + "200": { + "description": "Payout created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateResponse" } } } }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ { - "type": "object", - "required": [ - "becs_bank_debit" - ], - "properties": { - "becs_bank_debit": { - "type": "object", - "required": [ - "billing_details", - "account_number", - "bsb_number" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/BankDebitBilling" - }, - "account_number": { - "type": "string", - "description": "Account number for Becs payment method", - "example": "000123456" - }, - "bsb_number": { - "type": "string", - "description": "Bank-State-Branch (bsb) number", - "example": "000000" - }, - "bank_account_holder_name": { - "type": "string", - "description": "Owner name for bank debit", - "example": "A. Schneider", - "nullable": true - } + "api_key": [] + } + ] + } + }, + "/payouts/{payout_id}": { + "get": { + "tags": [ + "Payouts" + ], + "summary": "Payouts - Retrieve", + "description": "Payouts - Retrieve", + "operationId": "Retrieve a Payout", + "parameters": [ + { + "name": "payout_id", + "in": "path", + "description": "The identifier for payout]", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Payout retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateResponse" } } } }, + "404": { + "description": "Payout does not exist in our records" + } + }, + "security": [ { - "type": "object", - "required": [ - "bacs_bank_debit" - ], - "properties": { - "bacs_bank_debit": { - "type": "object", - "required": [ - "billing_details", - "account_number", - "sort_code", - "bank_account_holder_name" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/BankDebitBilling" - }, - "account_number": { - "type": "string", - "description": "Account number for Bacs payment method", - "example": "00012345" - }, - "sort_code": { - "type": "string", - "description": "Sort code for Bacs payment method", - "example": "108800" - }, - "bank_account_holder_name": { - "type": "string", - "description": "holder name for bank debit", - "example": "A. Schneider" - } + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "Payouts" + ], + "summary": "Payouts - Update", + "description": "Payouts - Update", + "operationId": "Update a Payout", + "parameters": [ + { + "name": "payout_id", + "in": "path", + "description": "The identifier for payout]", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payout updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateResponse" } } } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "api_key": [] } ] - }, - "BankNames": { - "type": "string", - "description": "Name of banks supported by Hyperswitch", - "enum": [ - "american_express", - "affin_bank", - "agro_bank", - "alliance_bank", - "am_bank", - "bank_of_america", - "bank_islam", - "bank_muamalat", - "bank_rakyat", - "bank_simpanan_nasional", - "barclays", - "blik_p_s_p", - "capital_one", - "chase", - "citi", - "cimb_bank", - "discover", - "navy_federal_credit_union", - "pentagon_federal_credit_union", - "synchrony_bank", - "wells_fargo", - "abn_amro", - "asn_bank", - "bunq", - "handelsbanken", - "hong_leong_bank", - "hsbc_bank", - "ing", - "knab", - "kuwait_finance_house", - "moneyou", - "rabobank", - "regiobank", - "revolut", - "sns_bank", - "triodos_bank", - "van_lanschot", - "arzte_und_apotheker_bank", - "austrian_anadi_bank_ag", - "bank_austria", - "bank99_ag", - "bankhaus_carl_spangler", - "bankhaus_schelhammer_und_schattera_ag", - "bank_millennium", - "bank_p_e_k_a_o_s_a", - "bawag_psk_ag", - "bks_bank_ag", - "brull_kallmus_bank_ag", - "btv_vier_lander_bank", - "capital_bank_grawe_gruppe_ag", - "ceska_sporitelna", - "dolomitenbank", - "easybank_ag", - "e_platby_v_u_b", - "erste_bank_und_sparkassen", - "friesland_bank", - "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", - "komercni_banka", - "m_bank", - "marchfelder_bank", - "maybank", - "oberbank_ag", - "osterreichische_arzte_und_apothekerbank", - "ocbc_bank", - "pay_with_i_n_g", - "place_z_i_p_k_o", - "platnosc_online_karta_platnicza", - "posojilnica_bank_e_gen", - "postova_banka", - "public_bank", - "raiffeisen_bankengruppe_osterreich", - "rhb_bank", - "schelhammer_capital_bank_ag", - "standard_chartered_bank", - "schoellerbank_ag", - "sparda_bank_wien", - "sporo_pay", - "santander_przelew24", - "tatra_pay", - "viamo", - "volksbank_gruppe", - "volkskreditbank_ag", - "vr_bank_braunau", - "uob_bank", - "pay_with_alior_bank", - "banki_spoldzielcze", - "pay_with_inteligo", - "b_n_p_paribas_poland", - "bank_nowy_s_a", - "credit_agricole", - "pay_with_b_o_s", - "pay_with_citi_handlowy", - "pay_with_plus_bank", - "toyota_bank", - "velo_bank", - "e_transfer_pocztowy24", - "plus_bank", - "etransfer_pocztowy24", - "banki_spbdzielcze", - "bank_nowy_bfg_sa", - "getin_bank", - "blik", - "noble_pay", - "idea_bank", - "envelo_bank", - "nest_przelew", - "mbank_mtransfer", - "inteligo", - "pbac_z_ipko", - "bnp_paribas", - "bank_pekao_sa", - "volkswagen_bank", - "alior_bank", - "boz", - "bangkok_bank", - "krungsri_bank", - "krung_thai_bank", - "the_siam_commercial_bank", - "kasikorn_bank", - "open_bank_success", - "open_bank_failure", - "open_bank_cancelled", - "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" - ] - }, - "BankRedirectBilling": { - "type": "object", - "required": [ - "billing_name", - "email" + } + }, + "/payouts/{payout_id}/cancel": { + "post": { + "tags": [ + "Payouts" ], - "properties": { - "billing_name": { - "type": "string", - "description": "The name for which billing is issued", - "example": "John Doe" - }, - "email": { - "type": "string", - "description": "The billing email for bank redirect", - "example": "example@example.com" - } - } - }, - "BankRedirectData": { - "oneOf": [ + "summary": "Payouts - Cancel", + "description": "Payouts - Cancel", + "operationId": "Cancel a Payout", + "parameters": [ { - "type": "object", - "required": [ - "bancontact_card" - ], - "properties": { - "bancontact_card": { - "type": "object", - "required": [ - "card_number", - "card_exp_month", - "card_exp_year", - "card_holder_name" - ], - "properties": { - "card_number": { - "type": "string", - "description": "The card number", - "example": "4242424242424242" - }, - "card_exp_month": { - "type": "string", - "description": "The card's expiry month", - "example": "24" - }, - "card_exp_year": { - "type": "string", - "description": "The card's expiry year", - "example": "24" - }, - "card_holder_name": { - "type": "string", - "description": "The card holder's name", - "example": "John Test" - }, - "billing_details": { - "allOf": [ - { - "$ref": "#/components/schemas/BankRedirectBilling" - } - ], - "nullable": true - } - } - } + "name": "payout_id", + "in": "path", + "description": "The identifier for payout", + "required": true, + "schema": { + "type": "string" } - }, - { - "type": "object", - "required": [ - "bizum" - ], - "properties": { - "bizum": { - "type": "object" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutActionRequest" } } }, - { - "type": "object", - "required": [ - "blik" - ], - "properties": { - "blik": { - "type": "object", - "properties": { - "blik_code": { - "type": "string", - "nullable": true - } + "required": true + }, + "responses": { + "200": { + "description": "Payout cancelled", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateResponse" } } } }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ { - "type": "object", - "required": [ - "eps" - ], - "properties": { - "eps": { - "type": "object", - "required": [ - "billing_details", - "bank_name", - "country" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/BankRedirectBilling" - }, - "bank_name": { - "$ref": "#/components/schemas/BankNames" - }, - "country": { - "$ref": "#/components/schemas/CountryAlpha2" - } - } - } - } - }, + "api_key": [] + } + ] + } + }, + "/payouts/{payout_id}/fulfill": { + "post": { + "tags": [ + "Payouts" + ], + "summary": "Payouts - Fulfill", + "description": "Payouts - Fulfill", + "operationId": "Fulfill a Payout", + "parameters": [ { - "type": "object", - "required": [ - "giropay" - ], - "properties": { - "giropay": { - "type": "object", - "required": [ - "billing_details", - "country" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/BankRedirectBilling" - }, - "bank_account_bic": { - "type": "string", - "description": "Bank account details for Giropay\nBank account bic code", - "nullable": true - }, - "bank_account_iban": { - "type": "string", - "description": "Bank account iban", - "nullable": true - }, - "country": { - "$ref": "#/components/schemas/CountryAlpha2" - } - } - } + "name": "payout_id", + "in": "path", + "description": "The identifier for payout", + "required": true, + "schema": { + "type": "string" } - }, - { - "type": "object", - "required": [ - "ideal" - ], - "properties": { - "ideal": { - "type": "object", - "required": [ - "billing_details", - "bank_name", - "country" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/BankRedirectBilling" - }, - "bank_name": { - "$ref": "#/components/schemas/BankNames" - }, - "country": { - "$ref": "#/components/schemas/CountryAlpha2" - } - } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutActionRequest" } } }, - { - "type": "object", - "required": [ - "interac" - ], - "properties": { - "interac": { - "type": "object", - "required": [ - "country", - "email" - ], - "properties": { - "country": { - "$ref": "#/components/schemas/CountryAlpha2" - }, - "email": { - "type": "string", - "example": "john.doe@example.com" - } + "required": true + }, + "responses": { + "200": { + "description": "Payout fulfilled", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateResponse" } } } }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ { - "type": "object", - "required": [ - "online_banking_czech_republic" - ], - "properties": { - "online_banking_czech_republic": { - "type": "object", - "required": [ - "issuer" - ], - "properties": { - "issuer": { - "$ref": "#/components/schemas/BankNames" + "api_key": [] + } + ] + } + }, + "/refunds": { + "post": { + "tags": [ + "Refunds" + ], + "summary": "Refunds - Create", + "description": "Refunds - Create\n\nCreates a refund against an already processed payment. In case of some processors, you can even opt to refund only a partial amount multiple times until the original charge amount has been refunded", + "operationId": "Create a Refund", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundRequest" + }, + "examples": { + "Create an instant refund to refund partial amount": { + "value": { + "amount": 654, + "payment_id": "{{payment_id}}", + "refund_type": "instant" + } + }, + "Create an instant refund to refund the whole amount": { + "value": { + "payment_id": "{{payment_id}}", + "refund_type": "instant" + } + }, + "Create an instant refund with reason": { + "value": { + "amount": 6540, + "payment_id": "{{payment_id}}", + "reason": "Customer returned product", + "refund_type": "instant" } } } } }, - { - "type": "object", - "required": [ - "online_banking_finland" - ], - "properties": { - "online_banking_finland": { - "type": "object", - "properties": { - "email": { - "type": "string", - "nullable": true - } + "required": true + }, + "responses": { + "200": { + "description": "Refund created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundResponse" } } } }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ { - "type": "object", - "required": [ - "online_banking_poland" - ], - "properties": { - "online_banking_poland": { - "type": "object", - "required": [ - "issuer" - ], - "properties": { - "issuer": { - "$ref": "#/components/schemas/BankNames" - } - } + "api_key": [] + } + ] + } + }, + "/refunds/list": { + "post": { + "tags": [ + "Refunds" + ], + "summary": "Refunds - List", + "description": "Refunds - List\n\nLists all the refunds associated with the merchant or a payment_id if payment_id is not provided", + "operationId": "List all Refunds", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundListRequest" } } }, - { - "type": "object", - "required": [ - "online_banking_slovakia" - ], - "properties": { - "online_banking_slovakia": { - "type": "object", - "required": [ - "issuer" - ], - "properties": { - "issuer": { - "$ref": "#/components/schemas/BankNames" - } + "required": true + }, + "responses": { + "200": { + "description": "List of refunds", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundListResponse" } } } - }, - { - "type": "object", - "required": [ - "open_banking_uk" - ], - "properties": { - "open_banking_uk": { - "type": "object", - "required": [ - "issuer", - "country" - ], - "properties": { - "issuer": { - "$ref": "#/components/schemas/BankNames" - }, - "country": { - "$ref": "#/components/schemas/CountryAlpha2" - } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/refunds/{refund_id}": { + "get": { + "tags": [ + "Refunds" + ], + "summary": "Refunds - Retrieve", + "description": "Refunds - Retrieve\n\nRetrieves a Refund. This may be used to get the status of a previously initiated refund", + "operationId": "Retrieve a Refund", + "parameters": [ + { + "name": "refund_id", + "in": "path", + "description": "The identifier for refund", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Refund retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundResponse" } } } }, + "404": { + "description": "Refund does not exist in our records" + } + }, + "security": [ { - "type": "object", - "required": [ - "przelewy24" - ], - "properties": { - "przelewy24": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "bank_name": { - "allOf": [ - { - "$ref": "#/components/schemas/BankNames" - } - ], - "nullable": true - }, - "billing_details": { - "$ref": "#/components/schemas/BankRedirectBilling" + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "Refunds" + ], + "summary": "Refunds - Update", + "description": "Refunds - Update\n\nUpdates the properties of a Refund object. This API can be used to attach a reason for the refund or metadata fields", + "operationId": "Update a Refund", + "parameters": [ + { + "name": "refund_id", + "in": "path", + "description": "The identifier for refund", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundUpdateRequest" + }, + "examples": { + "Update refund reason": { + "value": { + "reason": "Paid by mistake" } } } } }, - { - "type": "object", - "required": [ - "sofort" - ], - "properties": { - "sofort": { - "type": "object", - "required": [ - "billing_details", - "country", - "preferred_language" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/BankRedirectBilling" - }, - "country": { - "$ref": "#/components/schemas/CountryAlpha2" - }, - "preferred_language": { - "type": "string", - "description": "The preferred language", - "example": "en" - } + "required": true + }, + "responses": { + "200": { + "description": "Refund updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundResponse" } } } }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ { - "type": "object", - "required": [ - "trustly" - ], - "properties": { - "trustly": { - "type": "object", - "required": [ - "country" - ], - "properties": { - "country": { - "$ref": "#/components/schemas/CountryAlpha2" - } - } - } + "api_key": [] + } + ] + } + }, + "/routing": { + "get": { + "tags": [ + "Routing" + ], + "summary": "Routing - List", + "description": "Routing - List\n\nList all routing configs", + "operationId": "List routing configs", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "The number of records to be returned", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 } }, { - "type": "object", - "required": [ - "online_banking_fpx" - ], - "properties": { - "online_banking_fpx": { - "type": "object", - "required": [ - "issuer" - ], - "properties": { - "issuer": { - "$ref": "#/components/schemas/BankNames" - } - } - } + "name": "offset", + "in": "query", + "description": "The record offset from which to start gathering of results", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 } }, { - "type": "object", - "required": [ - "online_banking_thailand" - ], - "properties": { - "online_banking_thailand": { - "type": "object", - "required": [ - "issuer" - ], - "properties": { - "issuer": { - "$ref": "#/components/schemas/BankNames" - } + "name": "profile_id", + "in": "query", + "description": "The unique identifier for a merchant profile", + "required": false, + "schema": { + "type": "string", + "nullable": true + } + } + ], + "responses": { + "200": { + "description": "Successfully fetched routing configs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingKind" } } } + }, + "404": { + "description": "Resource missing" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] } ] }, - "BankTransferData": { - "oneOf": [ - { - "type": "object", - "required": [ - "ach_bank_transfer" - ], - "properties": { - "ach_bank_transfer": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/AchBillingDetails" - } - } + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Create", + "description": "Routing - Create\n\nCreate a routing config", + "operationId": "Create a routing config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingConfigRequest" } } }, - { - "type": "object", - "required": [ - "sepa_bank_transfer" - ], - "properties": { - "sepa_bank_transfer": { - "type": "object", - "required": [ - "billing_details", - "country" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/SepaAndBacsBillingDetails" - }, - "country": { - "$ref": "#/components/schemas/CountryAlpha2" - } + "required": true + }, + "responses": { + "200": { + "description": "Routing config created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" } } } }, + "400": { + "description": "Request body is malformed" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ { - "type": "object", - "required": [ - "bacs_bank_transfer" - ], - "properties": { - "bacs_bank_transfer": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/SepaAndBacsBillingDetails" - } - } - } - } + "api_key": [] }, { - "type": "object", - "required": [ - "multibanco_bank_transfer" - ], - "properties": { - "multibanco_bank_transfer": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/MultibancoBillingDetails" - } + "jwt_key": [] + } + ] + } + }, + "/routing/active": { + "get": { + "tags": [ + "Routing" + ], + "summary": "Routing - Retrieve Config", + "description": "Routing - Retrieve Config\n\nRetrieve active config", + "operationId": "Retrieve active config", + "parameters": [ + { + "name": "profile_id", + "in": "query", + "description": "The unique identifier for a merchant profile", + "required": false, + "schema": { + "type": "string", + "nullable": true + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved active config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkedRoutingConfigRetrieveResponse" } } } }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ { - "type": "object", - "required": [ - "permata_bank_transfer" - ], - "properties": { - "permata_bank_transfer": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/DokuBillingDetails" - } - } - } - } + "api_key": [] }, { - "type": "object", - "required": [ - "bca_bank_transfer" - ], - "properties": { - "bca_bank_transfer": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/DokuBillingDetails" - } - } + "jwt_key": [] + } + ] + } + }, + "/routing/deactivate": { + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Deactivate", + "description": "Routing - Deactivate\n\nDeactivates a routing config", + "operationId": "Deactivate a routing config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingConfigRequest" } } }, - { - "type": "object", - "required": [ - "bni_va_bank_transfer" - ], - "properties": { - "bni_va_bank_transfer": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/DokuBillingDetails" - } + "required": true + }, + "responses": { + "200": { + "description": "Successfully deactivated routing config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" } } } }, + "400": { + "description": "Malformed request" + }, + "403": { + "description": "Malformed request" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ { - "type": "object", - "required": [ - "bri_va_bank_transfer" - ], - "properties": { - "bri_va_bank_transfer": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/DokuBillingDetails" + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/default": { + "get": { + "tags": [ + "Routing" + ], + "summary": "Routing - Retrieve Default Config", + "description": "Routing - Retrieve Default Config\n\nRetrieve default fallback config", + "operationId": "Retrieve default fallback config", + "responses": { + "200": { + "description": "Successfully retrieved default config", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" } } } } }, + "500": { + "description": "Internal server error" + } + }, + "security": [ { - "type": "object", - "required": [ - "cimb_va_bank_transfer" - ], - "properties": { - "cimb_va_bank_transfer": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/DokuBillingDetails" - } + "api_key": [] + }, + { + "jwt_key": [] + } + ] + }, + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Update Default Config", + "description": "Routing - Update Default Config\n\nUpdate default fallback config", + "operationId": "Update default fallback config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" } } } }, - { - "type": "object", - "required": [ - "danamon_va_bank_transfer" - ], - "properties": { - "danamon_va_bank_transfer": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/DokuBillingDetails" + "required": true + }, + "responses": { + "200": { + "description": "Successfully updated default config", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" } } } } }, + "400": { + "description": "Malformed request" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ { - "type": "object", - "required": [ - "mandiri_va_bank_transfer" - ], - "properties": { - "mandiri_va_bank_transfer": { - "type": "object", - "required": [ - "billing_details" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/DokuBillingDetails" - } + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/default/profile": { + "get": { + "tags": [ + "Routing" + ], + "summary": "Routing - Retrieve Default For Profile", + "description": "Routing - Retrieve Default For Profile\n\nRetrieve default config for profiles", + "operationId": "Retrieve default configs for all profiles", + "responses": { + "200": { + "description": "Successfully retrieved default config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileDefaultRoutingConfig" } } } }, + "404": { + "description": "Resource missing" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, { - "type": "object", - "required": [ - "pix" - ], - "properties": { - "pix": { - "type": "object" + "jwt_key": [] + } + ] + } + }, + "/routing/default/profile/{profile_id}": { + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Update Default For Profile", + "description": "Routing - Update Default For Profile\n\nUpdate default config for profiles", + "operationId": "Update default configs for all profiles", + "parameters": [ + { + "name": "profile_id", + "in": "path", + "description": "The unique identifier for a profile", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successfully updated default config for profile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileDefaultRoutingConfig" + } } } }, + "400": { + "description": "Malformed request" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ { - "type": "object", - "required": [ - "pse" - ], - "properties": { - "pse": { - "type": "object" + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/{algorithm_id}": { + "get": { + "tags": [ + "Routing" + ], + "summary": "Routing - Retrieve", + "description": "Routing - Retrieve\n\nRetrieve a routing algorithm", + "operationId": "Retrieve a routing config", + "parameters": [ + { + "name": "algorithm_id", + "in": "path", + "description": "The unique identifier for a config", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully fetched routing config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantRoutingAlgorithm" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/{algorithm_id}/activate": { + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Activate config", + "description": "Routing - Activate config\n\nActivate a routing config", + "operationId": "Activate a routing config", + "parameters": [ + { + "name": "algorithm_id", + "in": "path", + "description": "The unique identifier for a config", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Routing config activated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } } } + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "Resource missing" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] } ] + } + } + }, + "components": { + "schemas": { + "AcceptanceType": { + "type": "string", + "description": "This is used to indicate if the mandate was accepted online or offline", + "enum": [ + "online", + "offline" + ] }, - "BankTransferInstructions": { + "AcceptedCountries": { "oneOf": [ { "type": "object", "required": [ - "doku_bank_transfer_instructions" + "type", + "list" ], "properties": { - "doku_bank_transfer_instructions": { - "$ref": "#/components/schemas/DokuBankTransferInstructions" + "type": { + "type": "string", + "enum": [ + "enable_only" + ] + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountryAlpha2" + } } } }, { "type": "object", "required": [ - "ach_credit_transfer" + "type", + "list" ], "properties": { - "ach_credit_transfer": { - "$ref": "#/components/schemas/AchTransfer" + "type": { + "type": "string", + "enum": [ + "disable_only" + ] + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountryAlpha2" + } } } }, { "type": "object", "required": [ - "sepa_bank_instructions" + "type" ], "properties": { - "sepa_bank_instructions": { - "$ref": "#/components/schemas/SepaBankTransferInstructions" + "type": { + "type": "string", + "enum": [ + "all_accepted" + ] } } - }, + } + ], + "description": "Object to filter the customer countries for which the payment method is displayed", + "discriminator": { + "propertyName": "type" + } + }, + "AcceptedCurrencies": { + "oneOf": [ { "type": "object", "required": [ - "bacs_bank_instructions" + "type", + "list" ], "properties": { - "bacs_bank_instructions": { - "$ref": "#/components/schemas/BacsBankTransferInstructions" + "type": { + "type": "string", + "enum": [ + "enable_only" + ] + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + } } } }, { "type": "object", "required": [ - "multibanco" + "type", + "list" ], "properties": { - "multibanco": { - "$ref": "#/components/schemas/MultibancoTransferInstructions" - } - } - } - ] - }, - "BankTransferNextStepsData": { - "allOf": [ - { - "$ref": "#/components/schemas/BankTransferInstructions" + "type": { + "type": "string", + "enum": [ + "disable_only" + ] + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + } + } + } }, { "type": "object", + "required": [ + "type" + ], "properties": { - "receiver": { - "allOf": [ - { - "$ref": "#/components/schemas/ReceiverDetails" - } - ], - "nullable": true + "type": { + "type": "string", + "enum": [ + "all_accepted" + ] } } } - ] - }, - "BoletoVoucherData": { - "type": "object", - "properties": { - "social_security_number": { - "type": "string", - "description": "The shopper's social security number", - "nullable": true - } + ], + "discriminator": { + "propertyName": "type" } }, - "CaptureMethod": { - "type": "string", - "enum": [ - "automatic", - "manual", - "manual_multiple", - "scheduled" - ] - }, - "CaptureResponse": { + "AchBankTransfer": { "type": "object", "required": [ - "capture_id", - "status", - "amount", - "connector", - "authorized_attempt_id", - "capture_sequence" + "bank_account_number", + "bank_routing_number" ], "properties": { - "capture_id": { + "bank_name": { "type": "string", - "description": "unique identifier for the capture" - }, - "status": { - "$ref": "#/components/schemas/CaptureStatus" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The capture amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.," + "description": "Bank name", + "example": "Deutsche Bank", + "nullable": true }, - "currency": { + "bank_country_code": { "allOf": [ { - "$ref": "#/components/schemas/Currency" + "$ref": "#/components/schemas/CountryAlpha2" } ], "nullable": true }, - "connector": { - "type": "string", - "description": "The connector used for the payment" - }, - "authorized_attempt_id": { - "type": "string", - "description": "unique identifier for the parent attempt on which this capture is made" - }, - "connector_capture_id": { - "type": "string", - "description": "A unique identifier for a capture provided by the connector", - "nullable": true - }, - "capture_sequence": { - "type": "integer", - "format": "int32", - "description": "sequence number of this capture" - }, - "error_message": { - "type": "string", - "description": "If there was an error while calling the connector the error message is received here", - "nullable": true - }, - "error_code": { + "bank_city": { "type": "string", - "description": "If there was an error while calling the connectors the code is received here", + "description": "Bank city", + "example": "California", "nullable": true }, - "error_reason": { + "bank_account_number": { "type": "string", - "description": "If there was an error while calling the connectors the reason is received here", - "nullable": true + "description": "Bank account number is an unique identifier assigned by a bank to a customer.", + "example": "000123456" }, - "reference_id": { + "bank_routing_number": { "type": "string", - "description": "reference to the capture at connector side", - "nullable": true + "description": "[9 digits] Routing number - used in USA for identifying a specific bank.", + "example": "110000000" } } }, - "CaptureStatus": { - "type": "string", - "enum": [ - "started", - "charged", - "pending", - "failed" - ] - }, - "Card": { + "AchBillingDetails": { "type": "object", "required": [ - "card_number", - "expiry_month", - "expiry_year", - "card_holder_name" + "email" ], "properties": { - "card_number": { - "type": "string", - "description": "The card number", - "example": "4242424242424242" - }, - "expiry_month": { - "type": "string", - "description": "The card's expiry month" - }, - "expiry_year": { - "type": "string", - "description": "The card's expiry year" - }, - "card_holder_name": { + "email": { "type": "string", - "description": "The card holder's name", - "example": "John Doe" + "description": "The Email ID for ACH billing", + "example": "example@me.com" } } }, - "CardDetail": { + "AchTransfer": { "type": "object", "required": [ - "card_number", - "card_exp_month", - "card_exp_year", - "card_holder_name" + "account_number", + "bank_name", + "routing_number", + "swift_code" ], "properties": { - "card_number": { + "account_number": { "type": "string", - "description": "Card Number", - "example": "4111111145551142" + "example": "122385736258" }, - "card_exp_month": { - "type": "string", - "description": "Card Expiry Month", - "example": "10" + "bank_name": { + "type": "string" }, - "card_exp_year": { + "routing_number": { "type": "string", - "description": "Card Expiry Year", - "example": "25" + "example": "012" }, - "card_holder_name": { + "swift_code": { "type": "string", - "description": "Card Holder Name", - "example": "John Doe" + "example": "234" + } + } + }, + "Address": { + "type": "object", + "properties": { + "address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], + "nullable": true }, - "nick_name": { - "type": "string", - "description": "Card Holder's Nick Name", - "example": "John Doe", + "phone": { + "allOf": [ + { + "$ref": "#/components/schemas/PhoneDetails" + } + ], "nullable": true } } }, - "CardDetailFromLocker": { + "AddressDetails": { "type": "object", + "description": "Address details", "properties": { - "scheme": { + "city": { "type": "string", + "description": "The address city", + "example": "New York", + "nullable": true, + "maxLength": 50 + }, + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], "nullable": true }, - "issuer_country": { + "line1": { "type": "string", - "nullable": true + "description": "The first line of the address", + "example": "123, King Street", + "nullable": true, + "maxLength": 200 }, - "last4_digits": { + "line2": { "type": "string", - "nullable": true + "description": "The second line of the address", + "example": "Powelson Avenue", + "nullable": true, + "maxLength": 50 }, - "expiry_month": { + "line3": { "type": "string", - "nullable": true + "description": "The third line of the address", + "example": "Bridgewater", + "nullable": true, + "maxLength": 50 }, - "expiry_year": { + "zip": { "type": "string", - "nullable": true + "description": "The zip/postal code for the address", + "example": "08807", + "nullable": true, + "maxLength": 50 }, - "card_token": { + "state": { "type": "string", + "description": "The address state", + "example": "New York", "nullable": true }, - "card_holder_name": { + "first_name": { "type": "string", - "nullable": true + "description": "The first name for the address", + "example": "John", + "nullable": true, + "maxLength": 255 }, - "card_fingerprint": { + "last_name": { + "type": "string", + "description": "The last name for the address", + "example": "Doe", + "nullable": true, + "maxLength": 255 + } + } + }, + "AirwallexData": { + "type": "object", + "properties": { + "payload": { "type": "string", + "description": "payload required by airwallex", "nullable": true + } + } + }, + "AlfamartVoucherData": { + "type": "object", + "required": [ + "first_name", + "last_name", + "email" + ], + "properties": { + "first_name": { + "type": "string", + "description": "The billing first name for Alfamart", + "example": "Jane" }, - "nick_name": { + "last_name": { "type": "string", - "nullable": true + "description": "The billing second name for Alfamart", + "example": "Doe" + }, + "email": { + "type": "string", + "description": "The Email ID for Alfamart", + "example": "example@me.com" } } }, - "CardNetwork": { - "type": "string", - "enum": [ - "Visa", - "Mastercard", - "AmericanExpress", - "JCB", - "DinersClub", - "Discover", - "CartesBancaires", - "UnionPay", - "Interac", - "RuPay", - "Maestro" - ] + "AliPayHkRedirection": { + "type": "object" }, - "CardRedirectData": { - "oneOf": [ - { - "type": "object", - "required": [ - "knet" - ], - "properties": { - "knet": { - "type": "object" - } - } + "AliPayQr": { + "type": "object" + }, + "AliPayRedirection": { + "type": "object" + }, + "AmountInfo": { + "type": "object", + "required": [ + "label", + "amount" + ], + "properties": { + "label": { + "type": "string", + "description": "The label must be the name of the merchant." }, - { - "type": "object", - "required": [ - "benefit" - ], - "properties": { - "benefit": { - "type": "object" - } - } + "type": { + "type": "string", + "description": "A value that indicates whether the line item(Ex: total, tax, discount, or grand total) is final or pending.", + "nullable": true }, + "amount": { + "type": "string", + "description": "The total amount for the payment" + } + } + }, + "ApiKeyExpiration": { + "oneOf": [ { - "type": "object", - "required": [ - "momo_atm" - ], - "properties": { - "momo_atm": { - "type": "object" - } - } + "type": "string", + "enum": [ + "never" + ] }, { - "type": "object", - "required": [ - "card_redirect" - ], - "properties": { - "card_redirect": { - "type": "object" - } - } + "type": "string", + "format": "date-time" } ] }, - "CashappQr": { - "type": "object" - }, - "Connector": { - "type": "string", - "enum": [ - "phonypay", - "fauxpay", - "pretendpay", - "stripe_test", - "adyen_test", - "checkout_test", - "paypal_test", - "aci", - "adyen", - "airwallex", - "authorizedotnet", - "bambora", - "bankofamerica", - "bitpay", - "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", - "prophetpay", - "rapyd", - "shift4", - "square", - "stax", - "stripe", - "trustpay", - "tsys", - "volt", - "wise", - "worldline", - "worldpay", - "zen", - "signifyd", - "plaid" - ] - }, - "ConnectorMetadata": { + "ApplePayPaymentRequest": { "type": "object", + "required": [ + "country_code", + "currency_code", + "total" + ], "properties": { - "apple_pay": { - "allOf": [ - { - "$ref": "#/components/schemas/ApplepayConnectorMetadataRequest" - } - ], + "country_code": { + "$ref": "#/components/schemas/CountryAlpha2" + }, + "currency_code": { + "$ref": "#/components/schemas/Currency" + }, + "total": { + "$ref": "#/components/schemas/AmountInfo" + }, + "merchant_capabilities": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of merchant capabilities(ex: whether capable of 3ds or no-3ds)", "nullable": true }, - "airwallex": { - "allOf": [ - { - "$ref": "#/components/schemas/AirwallexData" - } - ], + "supported_networks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of supported networks", "nullable": true }, - "noon": { - "allOf": [ - { - "$ref": "#/components/schemas/NoonData" - } - ], + "merchant_identifier": { + "type": "string", "nullable": true } } }, - "ConnectorType": { - "type": "string", - "enum": [ - "payment_processor", - "payment_vas", - "fin_operations", - "fiz_operations", - "networks", - "banking_entities", - "non_banking_finance", - "payout_processor", - "payment_method_auth" - ] - }, - "CountryAlpha2": { + "ApplePayRedirectData": { + "type": "object" + }, + "ApplePaySessionResponse": { + "oneOf": [ + { + "$ref": "#/components/schemas/ThirdPartySdkSessionResponse" + }, + { + "$ref": "#/components/schemas/NoThirdPartySdkSessionResponse" + }, + { + "type": "object", + "default": null, + "nullable": true + } + ] + }, + "ApplePayThirdPartySdkData": { + "type": "object" + }, + "ApplePayWalletData": { + "type": "object", + "required": [ + "payment_data", + "payment_method", + "transaction_identifier" + ], + "properties": { + "payment_data": { + "type": "string", + "description": "The payment data of Apple pay" + }, + "payment_method": { + "$ref": "#/components/schemas/ApplepayPaymentMethod" + }, + "transaction_identifier": { + "type": "string", + "description": "The unique identifier for the transaction" + } + } + }, + "ApplepayConnectorMetadataRequest": { + "type": "object", + "properties": { + "session_token_data": { + "allOf": [ + { + "$ref": "#/components/schemas/SessionTokenInfo" + } + ], + "nullable": true + } + } + }, + "ApplepayPaymentMethod": { + "type": "object", + "required": [ + "display_name", + "network", + "type" + ], + "properties": { + "display_name": { + "type": "string", + "description": "The name to be displayed on Apple Pay button" + }, + "network": { + "type": "string", + "description": "The network of the Apple pay payment method" + }, + "type": { + "type": "string", + "description": "The type of the payment method" + } + } + }, + "ApplepaySessionTokenResponse": { + "type": "object", + "required": [ + "session_token_data", + "connector", + "delayed_session_token", + "sdk_next_action" + ], + "properties": { + "session_token_data": { + "$ref": "#/components/schemas/ApplePaySessionResponse" + }, + "payment_request_data": { + "allOf": [ + { + "$ref": "#/components/schemas/ApplePayPaymentRequest" + } + ], + "nullable": true + }, + "connector": { + "type": "string", + "description": "The session token is w.r.t this connector" + }, + "delayed_session_token": { + "type": "boolean", + "description": "Identifier for the delayed session response" + }, + "sdk_next_action": { + "$ref": "#/components/schemas/SdkNextAction" + }, + "connector_reference_id": { + "type": "string", + "description": "The connector transaction id", + "nullable": true + }, + "connector_sdk_public_key": { + "type": "string", + "description": "The public key id is to invoke third party sdk", + "nullable": true + }, + "connector_merchant_id": { + "type": "string", + "description": "The connector merchant id", + "nullable": true + } + } + }, + "AttemptStatus": { "type": "string", "enum": [ - "AF", - "AX", - "AL", - "DZ", - "AS", - "AD", - "AO", - "AI", - "AQ", - "AG", - "AR", - "AM", - "AW", - "AU", - "AT", - "AZ", - "BS", - "BH", - "BD", - "BB", - "BY", - "BE", - "BZ", - "BJ", - "BM", - "BT", - "BO", - "BQ", - "BA", - "BW", - "BV", - "BR", - "IO", - "BN", - "BG", - "BF", - "BI", - "KH", - "CM", - "CA", - "CV", - "KY", - "CF", - "TD", - "CL", - "CN", - "CX", - "CC", - "CO", - "KM", - "CG", - "CD", - "CK", - "CR", - "CI", - "HR", - "CU", - "CW", - "CY", - "CZ", - "DK", - "DJ", - "DM", - "DO", - "EC", - "EG", - "SV", - "GQ", - "ER", - "EE", - "ET", - "FK", - "FO", - "FJ", - "FI", - "FR", - "GF", - "PF", - "TF", - "GA", - "GM", - "GE", - "DE", - "GH", - "GI", - "GR", - "GL", - "GD", - "GP", - "GU", - "GT", - "GG", - "GN", - "GW", - "GY", - "HT", - "HM", - "VA", - "HN", - "HK", - "HU", - "IS", - "IN", - "ID", - "IR", - "IQ", - "IE", - "IM", - "IL", - "IT", - "JM", - "JP", - "JE", - "JO", - "KZ", - "KE", - "KI", - "KP", - "KR", - "KW", - "KG", - "LA", - "LV", - "LB", - "LS", - "LR", - "LY", - "LI", - "LT", + "started", + "authentication_failed", + "router_declined", + "authentication_pending", + "authentication_successful", + "authorized", + "authorization_failed", + "charged", + "authorizing", + "cod_initiated", + "voided", + "void_initiated", + "capture_initiated", + "capture_failed", + "void_failed", + "auto_refunded", + "partial_charged", + "partial_charged_and_chargeable", + "unresolved", + "pending", + "failure", + "payment_method_awaited", + "confirmation_awaited", + "device_data_collection_pending" + ] + }, + "AuthenticationType": { + "type": "string", + "enum": [ + "three_ds", + "no_three_ds" + ] + }, + "AuthorizationStatus": { + "type": "string", + "enum": [ + "success", + "failure", + "processing", + "unresolved" + ] + }, + "BacsBankTransfer": { + "type": "object", + "required": [ + "bank_account_number", + "bank_sort_code" + ], + "properties": { + "bank_name": { + "type": "string", + "description": "Bank name", + "example": "Deutsche Bank", + "nullable": true + }, + "bank_country_code": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "bank_city": { + "type": "string", + "description": "Bank city", + "example": "California", + "nullable": true + }, + "bank_account_number": { + "type": "string", + "description": "Bank account number is an unique identifier assigned by a bank to a customer.", + "example": "000123456" + }, + "bank_sort_code": { + "type": "string", + "description": "[6 digits] Sort Code - used in UK and Ireland for identifying a bank and it's branches.", + "example": "98-76-54" + } + } + }, + "BacsBankTransferInstructions": { + "type": "object", + "required": [ + "account_holder_name", + "account_number", + "sort_code" + ], + "properties": { + "account_holder_name": { + "type": "string", + "example": "Jane Doe" + }, + "account_number": { + "type": "string", + "example": "10244123908" + }, + "sort_code": { + "type": "string", + "example": "012" + } + } + }, + "Bank": { + "oneOf": [ + { + "$ref": "#/components/schemas/AchBankTransfer" + }, + { + "$ref": "#/components/schemas/BacsBankTransfer" + }, + { + "$ref": "#/components/schemas/SepaBankTransfer" + } + ] + }, + "BankDebitBilling": { + "type": "object", + "required": [ + "name", + "email" + ], + "properties": { + "name": { + "type": "string", + "description": "The billing name for bank debits", + "example": "John Doe" + }, + "email": { + "type": "string", + "description": "The billing email for bank debits", + "example": "example@example.com" + }, + "address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], + "nullable": true + } + } + }, + "BankDebitData": { + "oneOf": [ + { + "type": "object", + "required": [ + "ach_bank_debit" + ], + "properties": { + "ach_bank_debit": { + "type": "object", + "description": "Payment Method data for Ach bank debit", + "required": [ + "billing_details", + "account_number", + "routing_number", + "card_holder_name", + "bank_account_holder_name", + "bank_name", + "bank_type", + "bank_holder_type" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/BankDebitBilling" + }, + "account_number": { + "type": "string", + "description": "Account number for ach bank debit payment", + "example": "000123456789" + }, + "routing_number": { + "type": "string", + "description": "Routing number for ach bank debit payment", + "example": "110000000" + }, + "card_holder_name": { + "type": "string", + "example": "John Test" + }, + "bank_account_holder_name": { + "type": "string", + "example": "John Doe" + }, + "bank_name": { + "type": "string", + "example": "ACH" + }, + "bank_type": { + "type": "string", + "example": "Checking" + }, + "bank_holder_type": { + "type": "string", + "example": "Personal" + } + } + } + } + }, + { + "type": "object", + "required": [ + "sepa_bank_debit" + ], + "properties": { + "sepa_bank_debit": { + "type": "object", + "required": [ + "billing_details", + "iban", + "bank_account_holder_name" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/BankDebitBilling" + }, + "iban": { + "type": "string", + "description": "International bank account number (iban) for SEPA", + "example": "DE89370400440532013000" + }, + "bank_account_holder_name": { + "type": "string", + "description": "Owner name for bank debit", + "example": "A. Schneider" + } + } + } + } + }, + { + "type": "object", + "required": [ + "becs_bank_debit" + ], + "properties": { + "becs_bank_debit": { + "type": "object", + "required": [ + "billing_details", + "account_number", + "bsb_number" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/BankDebitBilling" + }, + "account_number": { + "type": "string", + "description": "Account number for Becs payment method", + "example": "000123456" + }, + "bsb_number": { + "type": "string", + "description": "Bank-State-Branch (bsb) number", + "example": "000000" + }, + "bank_account_holder_name": { + "type": "string", + "description": "Owner name for bank debit", + "example": "A. Schneider", + "nullable": true + } + } + } + } + }, + { + "type": "object", + "required": [ + "bacs_bank_debit" + ], + "properties": { + "bacs_bank_debit": { + "type": "object", + "required": [ + "billing_details", + "account_number", + "sort_code", + "bank_account_holder_name" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/BankDebitBilling" + }, + "account_number": { + "type": "string", + "description": "Account number for Bacs payment method", + "example": "00012345" + }, + "sort_code": { + "type": "string", + "description": "Sort code for Bacs payment method", + "example": "108800" + }, + "bank_account_holder_name": { + "type": "string", + "description": "holder name for bank debit", + "example": "A. Schneider" + } + } + } + } + } + ] + }, + "BankNames": { + "type": "string", + "description": "Name of banks supported by Hyperswitch", + "enum": [ + "american_express", + "affin_bank", + "agro_bank", + "alliance_bank", + "am_bank", + "bank_of_america", + "bank_islam", + "bank_muamalat", + "bank_rakyat", + "bank_simpanan_nasional", + "barclays", + "blik_p_s_p", + "capital_one", + "chase", + "citi", + "cimb_bank", + "discover", + "navy_federal_credit_union", + "pentagon_federal_credit_union", + "synchrony_bank", + "wells_fargo", + "abn_amro", + "asn_bank", + "bunq", + "handelsbanken", + "hong_leong_bank", + "hsbc_bank", + "ing", + "knab", + "kuwait_finance_house", + "moneyou", + "rabobank", + "regiobank", + "revolut", + "sns_bank", + "triodos_bank", + "van_lanschot", + "arzte_und_apotheker_bank", + "austrian_anadi_bank_ag", + "bank_austria", + "bank99_ag", + "bankhaus_carl_spangler", + "bankhaus_schelhammer_und_schattera_ag", + "bank_millennium", + "bank_p_e_k_a_o_s_a", + "bawag_psk_ag", + "bks_bank_ag", + "brull_kallmus_bank_ag", + "btv_vier_lander_bank", + "capital_bank_grawe_gruppe_ag", + "ceska_sporitelna", + "dolomitenbank", + "easybank_ag", + "e_platby_v_u_b", + "erste_bank_und_sparkassen", + "friesland_bank", + "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", + "komercni_banka", + "m_bank", + "marchfelder_bank", + "maybank", + "oberbank_ag", + "osterreichische_arzte_und_apothekerbank", + "ocbc_bank", + "pay_with_i_n_g", + "place_z_i_p_k_o", + "platnosc_online_karta_platnicza", + "posojilnica_bank_e_gen", + "postova_banka", + "public_bank", + "raiffeisen_bankengruppe_osterreich", + "rhb_bank", + "schelhammer_capital_bank_ag", + "standard_chartered_bank", + "schoellerbank_ag", + "sparda_bank_wien", + "sporo_pay", + "santander_przelew24", + "tatra_pay", + "viamo", + "volksbank_gruppe", + "volkskreditbank_ag", + "vr_bank_braunau", + "uob_bank", + "pay_with_alior_bank", + "banki_spoldzielcze", + "pay_with_inteligo", + "b_n_p_paribas_poland", + "bank_nowy_s_a", + "credit_agricole", + "pay_with_b_o_s", + "pay_with_citi_handlowy", + "pay_with_plus_bank", + "toyota_bank", + "velo_bank", + "e_transfer_pocztowy24", + "plus_bank", + "etransfer_pocztowy24", + "banki_spbdzielcze", + "bank_nowy_bfg_sa", + "getin_bank", + "blik", + "noble_pay", + "idea_bank", + "envelo_bank", + "nest_przelew", + "mbank_mtransfer", + "inteligo", + "pbac_z_ipko", + "bnp_paribas", + "bank_pekao_sa", + "volkswagen_bank", + "alior_bank", + "boz", + "bangkok_bank", + "krungsri_bank", + "krung_thai_bank", + "the_siam_commercial_bank", + "kasikorn_bank", + "open_bank_success", + "open_bank_failure", + "open_bank_cancelled", + "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" + ] + }, + "BankRedirectBilling": { + "type": "object", + "required": [ + "billing_name", + "email" + ], + "properties": { + "billing_name": { + "type": "string", + "description": "The name for which billing is issued", + "example": "John Doe" + }, + "email": { + "type": "string", + "description": "The billing email for bank redirect", + "example": "example@example.com" + } + } + }, + "BankRedirectData": { + "oneOf": [ + { + "type": "object", + "required": [ + "bancontact_card" + ], + "properties": { + "bancontact_card": { + "type": "object", + "required": [ + "card_number", + "card_exp_month", + "card_exp_year", + "card_holder_name" + ], + "properties": { + "card_number": { + "type": "string", + "description": "The card number", + "example": "4242424242424242" + }, + "card_exp_month": { + "type": "string", + "description": "The card's expiry month", + "example": "24" + }, + "card_exp_year": { + "type": "string", + "description": "The card's expiry year", + "example": "24" + }, + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Test" + }, + "billing_details": { + "allOf": [ + { + "$ref": "#/components/schemas/BankRedirectBilling" + } + ], + "nullable": true + } + } + } + } + }, + { + "type": "object", + "required": [ + "bizum" + ], + "properties": { + "bizum": { + "type": "object" + } + } + }, + { + "type": "object", + "required": [ + "blik" + ], + "properties": { + "blik": { + "type": "object", + "properties": { + "blik_code": { + "type": "string", + "nullable": true + } + } + } + } + }, + { + "type": "object", + "required": [ + "eps" + ], + "properties": { + "eps": { + "type": "object", + "required": [ + "bank_name", + "country" + ], + "properties": { + "billing_details": { + "allOf": [ + { + "$ref": "#/components/schemas/BankRedirectBilling" + } + ], + "nullable": true + }, + "bank_name": { + "$ref": "#/components/schemas/BankNames" + }, + "country": { + "$ref": "#/components/schemas/CountryAlpha2" + } + } + } + } + }, + { + "type": "object", + "required": [ + "giropay" + ], + "properties": { + "giropay": { + "type": "object", + "required": [ + "billing_details", + "country" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/BankRedirectBilling" + }, + "bank_account_bic": { + "type": "string", + "description": "Bank account details for Giropay\nBank account bic code", + "nullable": true + }, + "bank_account_iban": { + "type": "string", + "description": "Bank account iban", + "nullable": true + }, + "country": { + "$ref": "#/components/schemas/CountryAlpha2" + } + } + } + } + }, + { + "type": "object", + "required": [ + "ideal" + ], + "properties": { + "ideal": { + "type": "object", + "required": [ + "bank_name", + "country" + ], + "properties": { + "billing_details": { + "allOf": [ + { + "$ref": "#/components/schemas/BankRedirectBilling" + } + ], + "nullable": true + }, + "bank_name": { + "$ref": "#/components/schemas/BankNames" + }, + "country": { + "$ref": "#/components/schemas/CountryAlpha2" + } + } + } + } + }, + { + "type": "object", + "required": [ + "interac" + ], + "properties": { + "interac": { + "type": "object", + "required": [ + "country", + "email" + ], + "properties": { + "country": { + "$ref": "#/components/schemas/CountryAlpha2" + }, + "email": { + "type": "string", + "example": "john.doe@example.com" + } + } + } + } + }, + { + "type": "object", + "required": [ + "online_banking_czech_republic" + ], + "properties": { + "online_banking_czech_republic": { + "type": "object", + "required": [ + "issuer" + ], + "properties": { + "issuer": { + "$ref": "#/components/schemas/BankNames" + } + } + } + } + }, + { + "type": "object", + "required": [ + "online_banking_finland" + ], + "properties": { + "online_banking_finland": { + "type": "object", + "properties": { + "email": { + "type": "string", + "nullable": true + } + } + } + } + }, + { + "type": "object", + "required": [ + "online_banking_poland" + ], + "properties": { + "online_banking_poland": { + "type": "object", + "required": [ + "issuer" + ], + "properties": { + "issuer": { + "$ref": "#/components/schemas/BankNames" + } + } + } + } + }, + { + "type": "object", + "required": [ + "online_banking_slovakia" + ], + "properties": { + "online_banking_slovakia": { + "type": "object", + "required": [ + "issuer" + ], + "properties": { + "issuer": { + "$ref": "#/components/schemas/BankNames" + } + } + } + } + }, + { + "type": "object", + "required": [ + "open_banking_uk" + ], + "properties": { + "open_banking_uk": { + "type": "object", + "required": [ + "issuer", + "country" + ], + "properties": { + "issuer": { + "$ref": "#/components/schemas/BankNames" + }, + "country": { + "$ref": "#/components/schemas/CountryAlpha2" + } + } + } + } + }, + { + "type": "object", + "required": [ + "przelewy24" + ], + "properties": { + "przelewy24": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "bank_name": { + "allOf": [ + { + "$ref": "#/components/schemas/BankNames" + } + ], + "nullable": true + }, + "billing_details": { + "$ref": "#/components/schemas/BankRedirectBilling" + } + } + } + } + }, + { + "type": "object", + "required": [ + "sofort" + ], + "properties": { + "sofort": { + "type": "object", + "required": [ + "country" + ], + "properties": { + "billing_details": { + "allOf": [ + { + "$ref": "#/components/schemas/BankRedirectBilling" + } + ], + "nullable": true + }, + "country": { + "$ref": "#/components/schemas/CountryAlpha2" + }, + "preferred_language": { + "type": "string", + "description": "The preferred language", + "example": "en", + "nullable": true + } + } + } + } + }, + { + "type": "object", + "required": [ + "trustly" + ], + "properties": { + "trustly": { + "type": "object", + "required": [ + "country" + ], + "properties": { + "country": { + "$ref": "#/components/schemas/CountryAlpha2" + } + } + } + } + }, + { + "type": "object", + "required": [ + "online_banking_fpx" + ], + "properties": { + "online_banking_fpx": { + "type": "object", + "required": [ + "issuer" + ], + "properties": { + "issuer": { + "$ref": "#/components/schemas/BankNames" + } + } + } + } + }, + { + "type": "object", + "required": [ + "online_banking_thailand" + ], + "properties": { + "online_banking_thailand": { + "type": "object", + "required": [ + "issuer" + ], + "properties": { + "issuer": { + "$ref": "#/components/schemas/BankNames" + } + } + } + } + } + ] + }, + "BankTransferData": { + "oneOf": [ + { + "type": "object", + "required": [ + "ach_bank_transfer" + ], + "properties": { + "ach_bank_transfer": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/AchBillingDetails" + } + } + } + } + }, + { + "type": "object", + "required": [ + "sepa_bank_transfer" + ], + "properties": { + "sepa_bank_transfer": { + "type": "object", + "required": [ + "billing_details", + "country" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/SepaAndBacsBillingDetails" + }, + "country": { + "$ref": "#/components/schemas/CountryAlpha2" + } + } + } + } + }, + { + "type": "object", + "required": [ + "bacs_bank_transfer" + ], + "properties": { + "bacs_bank_transfer": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/SepaAndBacsBillingDetails" + } + } + } + } + }, + { + "type": "object", + "required": [ + "multibanco_bank_transfer" + ], + "properties": { + "multibanco_bank_transfer": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/MultibancoBillingDetails" + } + } + } + } + }, + { + "type": "object", + "required": [ + "permata_bank_transfer" + ], + "properties": { + "permata_bank_transfer": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/DokuBillingDetails" + } + } + } + } + }, + { + "type": "object", + "required": [ + "bca_bank_transfer" + ], + "properties": { + "bca_bank_transfer": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/DokuBillingDetails" + } + } + } + } + }, + { + "type": "object", + "required": [ + "bni_va_bank_transfer" + ], + "properties": { + "bni_va_bank_transfer": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/DokuBillingDetails" + } + } + } + } + }, + { + "type": "object", + "required": [ + "bri_va_bank_transfer" + ], + "properties": { + "bri_va_bank_transfer": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/DokuBillingDetails" + } + } + } + } + }, + { + "type": "object", + "required": [ + "cimb_va_bank_transfer" + ], + "properties": { + "cimb_va_bank_transfer": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/DokuBillingDetails" + } + } + } + } + }, + { + "type": "object", + "required": [ + "danamon_va_bank_transfer" + ], + "properties": { + "danamon_va_bank_transfer": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/DokuBillingDetails" + } + } + } + } + }, + { + "type": "object", + "required": [ + "mandiri_va_bank_transfer" + ], + "properties": { + "mandiri_va_bank_transfer": { + "type": "object", + "required": [ + "billing_details" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/DokuBillingDetails" + } + } + } + } + }, + { + "type": "object", + "required": [ + "pix" + ], + "properties": { + "pix": { + "type": "object" + } + } + }, + { + "type": "object", + "required": [ + "pse" + ], + "properties": { + "pse": { + "type": "object" + } + } + } + ] + }, + "BankTransferInstructions": { + "oneOf": [ + { + "type": "object", + "required": [ + "doku_bank_transfer_instructions" + ], + "properties": { + "doku_bank_transfer_instructions": { + "$ref": "#/components/schemas/DokuBankTransferInstructions" + } + } + }, + { + "type": "object", + "required": [ + "ach_credit_transfer" + ], + "properties": { + "ach_credit_transfer": { + "$ref": "#/components/schemas/AchTransfer" + } + } + }, + { + "type": "object", + "required": [ + "sepa_bank_instructions" + ], + "properties": { + "sepa_bank_instructions": { + "$ref": "#/components/schemas/SepaBankTransferInstructions" + } + } + }, + { + "type": "object", + "required": [ + "bacs_bank_instructions" + ], + "properties": { + "bacs_bank_instructions": { + "$ref": "#/components/schemas/BacsBankTransferInstructions" + } + } + }, + { + "type": "object", + "required": [ + "multibanco" + ], + "properties": { + "multibanco": { + "$ref": "#/components/schemas/MultibancoTransferInstructions" + } + } + } + ] + }, + "BankTransferNextStepsData": { + "allOf": [ + { + "$ref": "#/components/schemas/BankTransferInstructions" + }, + { + "type": "object", + "properties": { + "receiver": { + "allOf": [ + { + "$ref": "#/components/schemas/ReceiverDetails" + } + ], + "nullable": true + } + } + } + ] + }, + "BlocklistDataKind": { + "type": "string", + "enum": [ + "payment_method", + "card_bin", + "extended_card_bin" + ] + }, + "BlocklistRequest": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "card_bin" + ] + }, + "data": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fingerprint" + ] + }, + "data": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "extended_card_bin" + ] + }, + "data": { + "type": "string" + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "BlocklistResponse": { + "type": "object", + "required": [ + "fingerprint_id", + "data_kind", + "created_at" + ], + "properties": { + "fingerprint_id": { + "type": "string" + }, + "data_kind": { + "$ref": "#/components/schemas/BlocklistDataKind" + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "BoletoVoucherData": { + "type": "object", + "properties": { + "social_security_number": { + "type": "string", + "description": "The shopper's social security number", + "nullable": true + } + } + }, + "BrowserInformation": { + "type": "object", + "description": "Browser information to be used for 3DS 2.0", + "properties": { + "color_depth": { + "type": "integer", + "format": "int32", + "description": "Color depth supported by the browser", + "nullable": true, + "minimum": 0 + }, + "java_enabled": { + "type": "boolean", + "description": "Whether java is enabled in the browser", + "nullable": true + }, + "java_script_enabled": { + "type": "boolean", + "description": "Whether javascript is enabled in the browser", + "nullable": true + }, + "language": { + "type": "string", + "description": "Language supported", + "nullable": true + }, + "screen_height": { + "type": "integer", + "format": "int32", + "description": "The screen height in pixels", + "nullable": true, + "minimum": 0 + }, + "screen_width": { + "type": "integer", + "format": "int32", + "description": "The screen width in pixels", + "nullable": true, + "minimum": 0 + }, + "time_zone": { + "type": "integer", + "format": "int32", + "description": "Time zone of the client", + "nullable": true + }, + "ip_address": { + "type": "string", + "description": "Ip address of the client", + "nullable": true + }, + "accept_header": { + "type": "string", + "description": "List of headers that are accepted", + "example": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "nullable": true + }, + "user_agent": { + "type": "string", + "description": "User-agent of the browser", + "nullable": true + } + } + }, + "BusinessPaymentLinkConfig": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentLinkConfigRequest" + }, + { + "type": "object", + "properties": { + "domain_name": { + "type": "string", + "nullable": true + } + } + } + ] + }, + "BusinessProfileCreate": { + "type": "object", + "properties": { + "profile_name": { + "type": "string", + "description": "The name of business profile", + "nullable": true, + "maxLength": 64 + }, + "return_url": { + "type": "string", + "description": "The URL to redirect after the completion of the operation", + "example": "https://www.example.com/success", + "nullable": true, + "maxLength": 255 + }, + "enable_payment_response_hash": { + "type": "boolean", + "description": "A boolean value to indicate if payment response hash needs to be enabled", + "default": true, + "example": true, + "nullable": true + }, + "payment_response_hash_key": { + "type": "string", + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used.", + "nullable": true + }, + "redirect_to_merchant_with_http_post": { + "type": "boolean", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "default": false, + "example": true, + "nullable": true + }, + "webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDetails" + } + ], + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "routing_algorithm": { + "type": "object", + "description": "The routing algorithm to be used for routing payments to desired connectors", + "nullable": true + }, + "intent_fulfillment_time": { + "type": "integer", + "format": "int32", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 + }, + "frm_routing_algorithm": { + "type": "object", + "description": "The frm routing algorithm to be used for routing payments to desired FRM's", + "nullable": true + }, + "payout_routing_algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + ], + "nullable": true + }, + "applepay_verified_domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Verified applepay domains for a particular profile", + "nullable": true + }, + "session_expiry": { + "type": "integer", + "format": "int32", + "description": "Client Secret Default expiry for all payments created under this business profile", + "example": 900, + "nullable": true, + "minimum": 0 + }, + "payment_link_config": { + "allOf": [ + { + "$ref": "#/components/schemas/BusinessPaymentLinkConfig" + } + ], + "nullable": true + } + } + }, + "BusinessProfileResponse": { + "type": "object", + "required": [ + "merchant_id", + "profile_id", + "profile_name", + "enable_payment_response_hash", + "redirect_to_merchant_with_http_post" + ], + "properties": { + "merchant_id": { + "type": "string", + "description": "The identifier for Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 64 + }, + "profile_id": { + "type": "string", + "description": "The default business profile that must be used for creating merchant accounts and payments", + "example": "pro_abcdefghijklmnopqrstuvwxyz", + "maxLength": 64 + }, + "profile_name": { + "type": "string", + "description": "Name of the business profile", + "maxLength": 64 + }, + "return_url": { + "type": "string", + "description": "The URL to redirect after the completion of the operation", + "example": "https://www.example.com/success", + "nullable": true, + "maxLength": 255 + }, + "enable_payment_response_hash": { + "type": "boolean", + "description": "A boolean value to indicate if payment response hash needs to be enabled", + "default": true, + "example": true + }, + "payment_response_hash_key": { + "type": "string", + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used.", + "nullable": true + }, + "redirect_to_merchant_with_http_post": { + "type": "boolean", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "default": false, + "example": true + }, + "webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDetails" + } + ], + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "routing_algorithm": { + "type": "object", + "description": "The routing algorithm to be used for routing payments to desired connectors", + "nullable": true + }, + "intent_fulfillment_time": { + "type": "integer", + "format": "int64", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true + }, + "frm_routing_algorithm": { + "type": "object", + "description": "The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom'", + "nullable": true + }, + "payout_routing_algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + ], + "nullable": true + }, + "applepay_verified_domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Verified applepay domains for a particular profile", + "nullable": true + }, + "session_expiry": { + "type": "integer", + "format": "int64", + "description": "Client Secret Default expiry for all payments created under this business profile", + "example": 900, + "nullable": true + }, + "payment_link_config": { + "description": "Default Payment Link config for all payment links created under this business profile", + "nullable": true + } + } + }, + "CaptureMethod": { + "type": "string", + "enum": [ + "automatic", + "manual", + "manual_multiple", + "scheduled" + ] + }, + "CaptureResponse": { + "type": "object", + "required": [ + "capture_id", + "status", + "amount", + "connector", + "authorized_attempt_id", + "capture_sequence" + ], + "properties": { + "capture_id": { + "type": "string", + "description": "unique identifier for the capture" + }, + "status": { + "$ref": "#/components/schemas/CaptureStatus" + }, + "amount": { + "type": "integer", + "format": "int64", + "description": "The capture amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.," + }, + "currency": { + "allOf": [ + { + "$ref": "#/components/schemas/Currency" + } + ], + "nullable": true + }, + "connector": { + "type": "string", + "description": "The connector used for the payment" + }, + "authorized_attempt_id": { + "type": "string", + "description": "unique identifier for the parent attempt on which this capture is made" + }, + "connector_capture_id": { + "type": "string", + "description": "A unique identifier for a capture provided by the connector", + "nullable": true + }, + "capture_sequence": { + "type": "integer", + "format": "int32", + "description": "sequence number of this capture" + }, + "error_message": { + "type": "string", + "description": "If there was an error while calling the connector the error message is received here", + "nullable": true + }, + "error_code": { + "type": "string", + "description": "If there was an error while calling the connectors the code is received here", + "nullable": true + }, + "error_reason": { + "type": "string", + "description": "If there was an error while calling the connectors the reason is received here", + "nullable": true + }, + "reference_id": { + "type": "string", + "description": "reference to the capture at connector side", + "nullable": true + } + } + }, + "CaptureStatus": { + "type": "string", + "enum": [ + "started", + "charged", + "pending", + "failed" + ] + }, + "Card": { + "type": "object", + "required": [ + "card_number", + "expiry_month", + "expiry_year", + "card_holder_name" + ], + "properties": { + "card_number": { + "type": "string", + "description": "The card number", + "example": "4242424242424242" + }, + "expiry_month": { + "type": "string", + "description": "The card's expiry month" + }, + "expiry_year": { + "type": "string", + "description": "The card's expiry year" + }, + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Doe" + } + } + }, + "CardDetail": { + "type": "object", + "required": [ + "card_number", + "card_exp_month", + "card_exp_year", + "card_holder_name" + ], + "properties": { + "card_number": { + "type": "string", + "description": "Card Number", + "example": "4111111145551142" + }, + "card_exp_month": { + "type": "string", + "description": "Card Expiry Month", + "example": "10" + }, + "card_exp_year": { + "type": "string", + "description": "Card Expiry Year", + "example": "25" + }, + "card_holder_name": { + "type": "string", + "description": "Card Holder Name", + "example": "John Doe" + }, + "nick_name": { + "type": "string", + "description": "Card Holder's Nick Name", + "example": "John Doe", + "nullable": true + }, + "card_issuing_country": { + "type": "string", + "description": "Card Issuing Country", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_issuer": { + "type": "string", + "description": "Issuer Bank for Card", + "nullable": true + }, + "card_type": { + "type": "string", + "description": "Card Type", + "nullable": true + } + } + }, + "CardDetailFromLocker": { + "type": "object", + "required": [ + "saved_to_locker" + ], + "properties": { + "scheme": { + "type": "string", + "nullable": true + }, + "issuer_country": { + "type": "string", + "nullable": true + }, + "last4_digits": { + "type": "string", + "nullable": true + }, + "expiry_month": { + "type": "string", + "nullable": true + }, + "expiry_year": { + "type": "string", + "nullable": true + }, + "card_token": { + "type": "string", + "nullable": true + }, + "card_holder_name": { + "type": "string", + "nullable": true + }, + "card_fingerprint": { + "type": "string", + "nullable": true + }, + "nick_name": { + "type": "string", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_isin": { + "type": "string", + "nullable": true + }, + "card_issuer": { + "type": "string", + "nullable": true + }, + "card_type": { + "type": "string", + "nullable": true + }, + "saved_to_locker": { + "type": "boolean" + } + } + }, + "CardNetwork": { + "type": "string", + "description": "Indicates the card network.", + "enum": [ + "Visa", + "Mastercard", + "AmericanExpress", + "JCB", + "DinersClub", + "Discover", + "CartesBancaires", + "UnionPay", + "Interac", + "RuPay", + "Maestro" + ] + }, + "CardRedirectData": { + "oneOf": [ + { + "type": "object", + "required": [ + "knet" + ], + "properties": { + "knet": { + "type": "object" + } + } + }, + { + "type": "object", + "required": [ + "benefit" + ], + "properties": { + "benefit": { + "type": "object" + } + } + }, + { + "type": "object", + "required": [ + "momo_atm" + ], + "properties": { + "momo_atm": { + "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" + }, + "card_cvc": { + "type": "string", + "description": "The CVC number for the card", + "nullable": true + } + } + }, + "CashappQr": { + "type": "object" + }, + "Comparison": { + "type": "object", + "description": "Represents a single comparison condition.", + "required": [ + "lhs", + "comparison", + "value", + "metadata" + ], + "properties": { + "lhs": { + "type": "string", + "description": "The left hand side which will always be a domain input identifier like \"payment.method.cardtype\"" + }, + "comparison": { + "$ref": "#/components/schemas/ComparisonType" + }, + "value": { + "$ref": "#/components/schemas/ValueType" + }, + "metadata": { + "type": "object", + "description": "Additional metadata that the Static Analyzer and Backend does not touch.\nThis can be used to store useful information for the frontend and is required for communication\nbetween the static analyzer and the frontend.", + "additionalProperties": {} + } + } + }, + "ComparisonType": { + "type": "string", + "description": "Conditional comparison type", + "enum": [ + "equal", + "not_equal", + "less_than", + "less_than_equal", + "greater_than", + "greater_than_equal" + ] + }, + "Connector": { + "type": "string", + "description": "A connector is an integration to fulfill payments", + "enum": [ + "aci", + "adyen", + "airwallex", + "authorizedotnet", + "bambora", + "bankofamerica", + "bitpay", + "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", + "placetopay", + "powertranz", + "prophetpay", + "rapyd", + "shift4", + "square", + "stax", + "stripe", + "trustpay", + "tsys", + "volt", + "wise", + "worldline", + "worldpay", + "zen", + "signifyd", + "plaid", + "riskified" + ] + }, + "ConnectorMetadata": { + "type": "object", + "properties": { + "apple_pay": { + "allOf": [ + { + "$ref": "#/components/schemas/ApplepayConnectorMetadataRequest" + } + ], + "nullable": true + }, + "airwallex": { + "allOf": [ + { + "$ref": "#/components/schemas/AirwallexData" + } + ], + "nullable": true + }, + "noon": { + "allOf": [ + { + "$ref": "#/components/schemas/NoonData" + } + ], + "nullable": true + } + } + }, + "ConnectorSelection": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "priority" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } + }, + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "volume_split" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectorVolumeSplit" + } + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "ConnectorStatus": { + "type": "string", + "enum": [ + "inactive", + "active" + ] + }, + "ConnectorType": { + "type": "string", + "description": "Type of the Connector for the financial use case. Could range from Payments to Accounting to Banking.", + "enum": [ + "payment_processor", + "payment_vas", + "fin_operations", + "fiz_operations", + "networks", + "banking_entities", + "non_banking_finance", + "payout_processor", + "payment_method_auth" + ] + }, + "ConnectorVolumeSplit": { + "type": "object", + "required": [ + "connector", + "split" + ], + "properties": { + "connector": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + }, + "split": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + }, + "CountryAlpha2": { + "type": "string", + "enum": [ + "AF", + "AX", + "AL", + "DZ", + "AS", + "AD", + "AO", + "AI", + "AQ", + "AG", + "AR", + "AM", + "AW", + "AU", + "AT", + "AZ", + "BS", + "BH", + "BD", + "BB", + "BY", + "BE", + "BZ", + "BJ", + "BM", + "BT", + "BO", + "BQ", + "BA", + "BW", + "BV", + "BR", + "IO", + "BN", + "BG", + "BF", + "BI", + "KH", + "CM", + "CA", + "CV", + "KY", + "CF", + "TD", + "CL", + "CN", + "CX", + "CC", + "CO", + "KM", + "CG", + "CD", + "CK", + "CR", + "CI", + "HR", + "CU", + "CW", + "CY", + "CZ", + "DK", + "DJ", + "DM", + "DO", + "EC", + "EG", + "SV", + "GQ", + "ER", + "EE", + "ET", + "FK", + "FO", + "FJ", + "FI", + "FR", + "GF", + "PF", + "TF", + "GA", + "GM", + "GE", + "DE", + "GH", + "GI", + "GR", + "GL", + "GD", + "GP", + "GU", + "GT", + "GG", + "GN", + "GW", + "GY", + "HT", + "HM", + "VA", + "HN", + "HK", + "HU", + "IS", + "IN", + "ID", + "IR", + "IQ", + "IE", + "IM", + "IL", + "IT", + "JM", + "JP", + "JE", + "JO", + "KZ", + "KE", + "KI", + "KP", + "KR", + "KW", + "KG", + "LA", + "LV", + "LB", + "LS", + "LR", + "LY", + "LI", + "LT", "LU", "MO", "MK", @@ -4414,3632 +6956,5067 @@ "US" ] }, - "CreateApiKeyRequest": { + "CreateApiKeyRequest": { + "type": "object", + "description": "The request body for creating an API Key.", + "required": [ + "name", + "expiration" + ], + "properties": { + "name": { + "type": "string", + "description": "A unique name for the API Key to help you identify it.", + "example": "Sandbox integration key", + "maxLength": 64 + }, + "description": { + "type": "string", + "description": "A description to provide more context about the API Key.", + "example": "Key used by our developers to integrate with the sandbox environment", + "nullable": true, + "maxLength": 256 + }, + "expiration": { + "$ref": "#/components/schemas/ApiKeyExpiration" + } + } + }, + "CreateApiKeyResponse": { + "type": "object", + "description": "The response body for creating an API Key.", + "required": [ + "key_id", + "merchant_id", + "name", + "api_key", + "created", + "expiration" + ], + "properties": { + "key_id": { + "type": "string", + "description": "The identifier for the API Key.", + "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", + "maxLength": 64 + }, + "merchant_id": { + "type": "string", + "description": "The identifier for the Merchant Account.", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 64 + }, + "name": { + "type": "string", + "description": "The unique name for the API Key to help you identify it.", + "example": "Sandbox integration key", + "maxLength": 64 + }, + "description": { + "type": "string", + "description": "The description to provide more context about the API Key.", + "example": "Key used by our developers to integrate with the sandbox environment", + "nullable": true, + "maxLength": 256 + }, + "api_key": { + "type": "string", + "description": "The plaintext API Key used for server-side API access. Ensure you store the API Key\nsecurely as you will not be able to see it again.", + "maxLength": 128 + }, + "created": { + "type": "string", + "format": "date-time", + "description": "The time at which the API Key was created.", + "example": "2022-09-10T10:11:12Z" + }, + "expiration": { + "$ref": "#/components/schemas/ApiKeyExpiration" + } + } + }, + "CryptoData": { + "type": "object", + "properties": { + "pay_currency": { + "type": "string", + "nullable": true + } + } + }, + "Currency": { + "type": "string", + "description": "The three letter ISO currency code in uppercase. Eg: 'USD' for the United States Dollar.", + "enum": [ + "AED", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BWP", + "BYN", + "BZD", + "CAD", + "CHF", + "CLP", + "CNY", + "COP", + "CRC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRU", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SEK", + "SGD", + "SHP", + "SLE", + "SLL", + "SOS", + "SRD", + "SSP", + "STN", + "SVC", + "SZL", + "THB", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VES", + "VND", + "VUV", + "WST", + "XAF", + "XCD", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMW" + ] + }, + "CustomerAcceptance": { + "type": "object", + "required": [ + "acceptance_type" + ], + "properties": { + "acceptance_type": { + "$ref": "#/components/schemas/AcceptanceType" + }, + "accepted_at": { + "type": "string", + "format": "date-time", + "description": "Specifying when the customer acceptance was provided", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, + "online": { + "allOf": [ + { + "$ref": "#/components/schemas/OnlineMandate" + } + ], + "nullable": true + } + } + }, + "CustomerDeleteResponse": { + "type": "object", + "required": [ + "customer_id", + "customer_deleted", + "address_deleted", + "payment_methods_deleted" + ], + "properties": { + "customer_id": { + "type": "string", + "description": "The identifier for the customer object", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 + }, + "customer_deleted": { + "type": "boolean", + "description": "Whether customer was deleted or not", + "example": false + }, + "address_deleted": { + "type": "boolean", + "description": "Whether address was deleted or not", + "example": false + }, + "payment_methods_deleted": { + "type": "boolean", + "description": "Whether payment methods deleted or not", + "example": false + } + } + }, + "CustomerDetails": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "The identifier for the customer." + }, + "name": { + "type": "string", + "description": "The customer's name", + "example": "John Doe", + "nullable": true, + "maxLength": 255 + }, + "email": { + "type": "string", + "description": "The customer's email address", + "example": "johntest@test.com", + "nullable": true, + "maxLength": 255 + }, + "phone": { + "type": "string", + "description": "The customer's phone number", + "example": "3141592653", + "nullable": true, + "maxLength": 10 + }, + "phone_country_code": { + "type": "string", + "description": "The country code for the customer's phone number", + "example": "+1", + "nullable": true, + "maxLength": 2 + } + } + }, + "CustomerPaymentMethod": { + "type": "object", + "required": [ + "payment_token", + "customer_id", + "payment_method", + "recurring_enabled", + "installment_payment_enabled", + "requires_cvv" + ], + "properties": { + "payment_token": { + "type": "string", + "description": "Token for payment method in temporary card locker which gets refreshed often", + "example": "7ebf443f-a050-4067-84e5-e6f6d4800aef" + }, + "customer_id": { + "type": "string", + "description": "The unique identifier of the customer.", + "example": "cus_meowerunwiuwiwqw" + }, + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } + ], + "nullable": true + }, + "payment_method_issuer": { + "type": "string", + "description": "The name of the bank/ provider issuing the payment method to the end user", + "example": "Citibank", + "nullable": true + }, + "payment_method_issuer_code": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodIssuerCode" + } + ], + "nullable": true + }, + "recurring_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method is eligible for recurring payments", + "example": true + }, + "installment_payment_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method is eligible for installment payments", + "example": true + }, + "payment_experience": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentExperience" + }, + "description": "Type of payment experience enabled with the connector", + "example": [ + "redirect_to_url" + ], + "nullable": true + }, + "card": { + "allOf": [ + { + "$ref": "#/components/schemas/CardDetailFromLocker" + } + ], + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "created": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the customer was created", + "example": "2023-01-18T11:04:09.922Z", + "nullable": true + }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" + } + ], + "nullable": true + }, + "bank": { + "allOf": [ + { + "$ref": "#/components/schemas/MaskedBankDetails" + } + ], + "nullable": true + }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeDetailsResponse" + } + ], + "nullable": true + }, + "requires_cvv": { + "type": "boolean", + "description": "Whether this payment method requires CVV to be collected", + "example": true + } + } + }, + "CustomerPaymentMethodsListResponse": { + "type": "object", + "required": [ + "customer_payment_methods" + ], + "properties": { + "customer_payment_methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerPaymentMethod" + }, + "description": "List of payment methods for customer" + } + } + }, + "CustomerRequest": { + "type": "object", + "description": "The customer details", + "properties": { + "customer_id": { + "type": "string", + "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 + }, + "name": { + "type": "string", + "description": "The customer's name", + "example": "Jon Test", + "nullable": true, + "maxLength": 255 + }, + "email": { + "type": "string", + "description": "The customer's email address", + "example": "JonTest@test.com", + "nullable": true, + "maxLength": 255 + }, + "phone": { + "type": "string", + "description": "The customer's phone number", + "example": "9999999999", + "nullable": true, + "maxLength": 255 + }, + "description": { + "type": "string", + "description": "An arbitrary string that you can attach to a customer object.", + "example": "First Customer", + "nullable": true, + "maxLength": 255 + }, + "phone_country_code": { + "type": "string", + "description": "The country code for the customer phone number", + "example": "+65", + "nullable": true, + "maxLength": 255 + }, + "address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", + "nullable": true + } + } + }, + "CustomerResponse": { + "type": "object", + "required": [ + "customer_id", + "created_at" + ], + "properties": { + "customer_id": { + "type": "string", + "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 + }, + "name": { + "type": "string", + "description": "The customer's name", + "example": "Jon Test", + "nullable": true, + "maxLength": 255 + }, + "email": { + "type": "string", + "description": "The customer's email address", + "example": "JonTest@test.com", + "nullable": true, + "maxLength": 255 + }, + "phone": { + "type": "string", + "description": "The customer's phone number", + "example": "9999999999", + "nullable": true, + "maxLength": 255 + }, + "phone_country_code": { + "type": "string", + "description": "The country code for the customer phone number", + "example": "+65", + "nullable": true, + "maxLength": 255 + }, + "description": { + "type": "string", + "description": "An arbitrary string that you can attach to a customer object.", + "example": "First Customer", + "nullable": true, + "maxLength": 255 + }, + "address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the customer was created", + "example": "2023-01-18T11:04:09.922Z" + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", + "nullable": true + } + } + }, + "DisputeResponse": { + "type": "object", + "required": [ + "dispute_id", + "payment_id", + "attempt_id", + "amount", + "currency", + "dispute_stage", + "dispute_status", + "connector", + "connector_status", + "connector_dispute_id", + "created_at" + ], + "properties": { + "dispute_id": { + "type": "string", + "description": "The identifier for dispute" + }, + "payment_id": { + "type": "string", + "description": "The identifier for payment_intent" + }, + "attempt_id": { + "type": "string", + "description": "The identifier for payment_attempt" + }, + "amount": { + "type": "string", + "description": "The dispute amount" + }, + "currency": { + "type": "string", + "description": "The three-letter ISO currency code" + }, + "dispute_stage": { + "$ref": "#/components/schemas/DisputeStage" + }, + "dispute_status": { + "$ref": "#/components/schemas/DisputeStatus" + }, + "connector": { + "type": "string", + "description": "connector to which dispute is associated with" + }, + "connector_status": { + "type": "string", + "description": "Status of the dispute sent by connector" + }, + "connector_dispute_id": { + "type": "string", + "description": "Dispute id sent by connector" + }, + "connector_reason": { + "type": "string", + "description": "Reason of dispute sent by connector", + "nullable": true + }, + "connector_reason_code": { + "type": "string", + "description": "Reason code of dispute sent by connector", + "nullable": true + }, + "challenge_required_by": { + "type": "string", + "format": "date-time", + "description": "Evidence deadline of dispute sent by connector", + "nullable": true + }, + "connector_created_at": { + "type": "string", + "format": "date-time", + "description": "Dispute created time sent by connector", + "nullable": true + }, + "connector_updated_at": { + "type": "string", + "format": "date-time", + "description": "Dispute updated time sent by connector", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Time at which dispute is received" + } + } + }, + "DisputeResponsePaymentsRetrieve": { + "type": "object", + "required": [ + "dispute_id", + "dispute_stage", + "dispute_status", + "connector_status", + "connector_dispute_id", + "created_at" + ], + "properties": { + "dispute_id": { + "type": "string", + "description": "The identifier for dispute" + }, + "dispute_stage": { + "$ref": "#/components/schemas/DisputeStage" + }, + "dispute_status": { + "$ref": "#/components/schemas/DisputeStatus" + }, + "connector_status": { + "type": "string", + "description": "Status of the dispute sent by connector" + }, + "connector_dispute_id": { + "type": "string", + "description": "Dispute id sent by connector" + }, + "connector_reason": { + "type": "string", + "description": "Reason of dispute sent by connector", + "nullable": true + }, + "connector_reason_code": { + "type": "string", + "description": "Reason code of dispute sent by connector", + "nullable": true + }, + "challenge_required_by": { + "type": "string", + "format": "date-time", + "description": "Evidence deadline of dispute sent by connector", + "nullable": true + }, + "connector_created_at": { + "type": "string", + "format": "date-time", + "description": "Dispute created time sent by connector", + "nullable": true + }, + "connector_updated_at": { + "type": "string", + "format": "date-time", + "description": "Dispute updated time sent by connector", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Time at which dispute is received" + } + } + }, + "DisputeStage": { + "type": "string", + "enum": [ + "pre_dispute", + "dispute", + "pre_arbitration" + ] + }, + "DisputeStatus": { + "type": "string", + "enum": [ + "dispute_opened", + "dispute_expired", + "dispute_accepted", + "dispute_cancelled", + "dispute_challenged", + "dispute_won", + "dispute_lost" + ] + }, + "DokuBankTransferInstructions": { + "type": "object", + "required": [ + "expires_at", + "reference", + "instructions_url" + ], + "properties": { + "expires_at": { + "type": "string", + "example": "1707091200000" + }, + "reference": { + "type": "string", + "example": "122385736258" + }, + "instructions_url": { + "type": "string" + } + } + }, + "DokuBillingDetails": { + "type": "object", + "required": [ + "first_name", + "last_name", + "email" + ], + "properties": { + "first_name": { + "type": "string", + "description": "The billing first name for Doku", + "example": "Jane" + }, + "last_name": { + "type": "string", + "description": "The billing second name for Doku", + "example": "Doe" + }, + "email": { + "type": "string", + "description": "The Email ID for Doku billing", + "example": "example@me.com" + } + } + }, + "EphemeralKeyCreateResponse": { + "type": "object", + "required": [ + "customer_id", + "created_at", + "expires", + "secret" + ], + "properties": { + "customer_id": { + "type": "string", + "description": "customer_id to which this ephemeral key belongs to" + }, + "created_at": { + "type": "integer", + "format": "int64", + "description": "time at which this ephemeral key was created" + }, + "expires": { + "type": "integer", + "format": "int64", + "description": "time at which this ephemeral key would expire" + }, + "secret": { + "type": "string", + "description": "ephemeral key" + } + } + }, + "EventType": { + "type": "string", + "enum": [ + "payment_succeeded", + "payment_failed", + "payment_processing", + "payment_cancelled", + "payment_authorized", + "payment_captured", + "action_required", + "refund_succeeded", + "refund_failed", + "dispute_opened", + "dispute_expired", + "dispute_accepted", + "dispute_cancelled", + "dispute_challenged", + "dispute_won", + "dispute_lost", + "mandate_active", + "mandate_revoked" + ] + }, + "FeatureMetadata": { + "type": "object", + "properties": { + "redirect_response": { + "allOf": [ + { + "$ref": "#/components/schemas/RedirectResponse" + } + ], + "nullable": true + } + } + }, + "FieldType": { + "oneOf": [ + { + "type": "string", + "enum": [ + "user_card_number" + ] + }, + { + "type": "string", + "enum": [ + "user_card_expiry_month" + ] + }, + { + "type": "string", + "enum": [ + "user_card_expiry_year" + ] + }, + { + "type": "string", + "enum": [ + "user_card_cvc" + ] + }, + { + "type": "string", + "enum": [ + "user_full_name" + ] + }, + { + "type": "string", + "enum": [ + "user_email_address" + ] + }, + { + "type": "string", + "enum": [ + "user_phone_number" + ] + }, + { + "type": "string", + "enum": [ + "user_country_code" + ] + }, + { + "type": "object", + "required": [ + "user_country" + ], + "properties": { + "user_country": { + "type": "object", + "required": [ + "options" + ], + "properties": { + "options": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "type": "object", + "required": [ + "user_currency" + ], + "properties": { + "user_currency": { + "type": "object", + "required": [ + "options" + ], + "properties": { + "options": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "type": "string", + "enum": [ + "user_billing_name" + ] + }, + { + "type": "string", + "enum": [ + "user_address_line1" + ] + }, + { + "type": "string", + "enum": [ + "user_address_line2" + ] + }, + { + "type": "string", + "enum": [ + "user_address_city" + ] + }, + { + "type": "string", + "enum": [ + "user_address_pincode" + ] + }, + { + "type": "string", + "enum": [ + "user_address_state" + ] + }, + { + "type": "object", + "required": [ + "user_address_country" + ], + "properties": { + "user_address_country": { + "type": "object", + "required": [ + "options" + ], + "properties": { + "options": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "type": "string", + "enum": [ + "user_blik_code" + ] + }, + { + "type": "string", + "enum": [ + "user_bank" + ] + }, + { + "type": "string", + "enum": [ + "text" + ] + }, + { + "type": "object", + "required": [ + "drop_down" + ], + "properties": { + "drop_down": { + "type": "object", + "required": [ + "options" + ], + "properties": { + "options": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + ], + "description": "Possible field type of required fields in payment_method_data" + }, + "FrmAction": { + "type": "string", + "enum": [ + "cancel_txn", + "auto_refund", + "manual_review" + ] + }, + "FrmConfigs": { + "type": "object", + "description": "Details of FrmConfigs are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", + "required": [ + "gateway", + "payment_methods" + ], + "properties": { + "gateway": { + "$ref": "#/components/schemas/ConnectorType" + }, + "payment_methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FrmPaymentMethod" + }, + "description": "payment methods that can be used in the payment" + } + } + }, + "FrmMessage": { + "type": "object", + "description": "frm message is an object sent inside the payments response...when frm is invoked, its value is Some(...), else its None", + "required": [ + "frm_name" + ], + "properties": { + "frm_name": { + "type": "string" + }, + "frm_transaction_id": { + "type": "string", + "nullable": true + }, + "frm_transaction_type": { + "type": "string", + "nullable": true + }, + "frm_status": { + "type": "string", + "nullable": true + }, + "frm_score": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "frm_reason": { + "nullable": true + }, + "frm_error": { + "type": "string", + "nullable": true + } + } + }, + "FrmPaymentMethod": { + "type": "object", + "description": "Details of FrmPaymentMethod are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", + "required": [ + "payment_method", + "payment_method_types" + ], + "properties": { + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FrmPaymentMethodType" + }, + "description": "payment method types(credit, debit) that can be used in the payment" + } + } + }, + "FrmPaymentMethodType": { + "type": "object", + "description": "Details of FrmPaymentMethodType are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", + "required": [ + "payment_method_type", + "card_networks", + "flow", + "action" + ], + "properties": { + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "card_networks": { + "$ref": "#/components/schemas/CardNetwork" + }, + "flow": { + "$ref": "#/components/schemas/FrmPreferredFlowTypes" + }, + "action": { + "$ref": "#/components/schemas/FrmAction" + } + } + }, + "FrmPreferredFlowTypes": { + "type": "string", + "enum": [ + "pre", + "post" + ] + }, + "FutureUsage": { + "type": "string", + "enum": [ + "off_session", + "on_session" + ] + }, + "GcashRedirection": { + "type": "object" + }, + "GiftCardData": { + "oneOf": [ + { + "type": "object", + "required": [ + "givex" + ], + "properties": { + "givex": { + "$ref": "#/components/schemas/GiftCardDetails" + } + } + }, + { + "type": "object", + "required": [ + "pay_safe_card" + ], + "properties": { + "pay_safe_card": { + "type": "object" + } + } + } + ] + }, + "GiftCardDetails": { + "type": "object", + "required": [ + "number", + "cvc" + ], + "properties": { + "number": { + "type": "string", + "description": "The gift card number" + }, + "cvc": { + "type": "string", + "description": "The card verification code." + } + } + }, + "GoPayRedirection": { + "type": "object" + }, + "GooglePayPaymentMethodInfo": { + "type": "object", + "required": [ + "card_network", + "card_details" + ], + "properties": { + "card_network": { + "type": "string", + "description": "The name of the card network" + }, + "card_details": { + "type": "string", + "description": "The details of the card" + } + } + }, + "GooglePayRedirectData": { + "type": "object" + }, + "GooglePaySessionResponse": { + "type": "object", + "required": [ + "merchant_info", + "allowed_payment_methods", + "transaction_info", + "delayed_session_token", + "connector", + "sdk_next_action" + ], + "properties": { + "merchant_info": { + "$ref": "#/components/schemas/GpayMerchantInfo" + }, + "allowed_payment_methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GpayAllowedPaymentMethods" + }, + "description": "List of the allowed payment meythods" + }, + "transaction_info": { + "$ref": "#/components/schemas/GpayTransactionInfo" + }, + "delayed_session_token": { + "type": "boolean", + "description": "Identifier for the delayed session response" + }, + "connector": { + "type": "string", + "description": "The name of the connector" + }, + "sdk_next_action": { + "$ref": "#/components/schemas/SdkNextAction" + }, + "secrets": { + "allOf": [ + { + "$ref": "#/components/schemas/SecretInfoToInitiateSdk" + } + ], + "nullable": true + } + } + }, + "GooglePayThirdPartySdk": { "type": "object", - "description": "The request body for creating an API Key.", "required": [ - "name", - "expiration" + "delayed_session_token", + "connector", + "sdk_next_action" ], "properties": { - "name": { + "delayed_session_token": { + "type": "boolean", + "description": "Identifier for the delayed session response" + }, + "connector": { "type": "string", - "description": "A unique name for the API Key to help you identify it.", - "example": "Sandbox integration key", - "maxLength": 64 + "description": "The name of the connector" + }, + "sdk_next_action": { + "$ref": "#/components/schemas/SdkNextAction" + } + } + }, + "GooglePayThirdPartySdkData": { + "type": "object" + }, + "GooglePayWalletData": { + "type": "object", + "required": [ + "type", + "description", + "info", + "tokenization_data" + ], + "properties": { + "type": { + "type": "string", + "description": "The type of payment method" }, "description": { "type": "string", - "description": "A description to provide more context about the API Key.", - "example": "Key used by our developers to integrate with the sandbox environment", - "nullable": true, - "maxLength": 256 + "description": "User-facing message to describe the payment method that funds this transaction." }, - "expiration": { - "$ref": "#/components/schemas/ApiKeyExpiration" + "info": { + "$ref": "#/components/schemas/GooglePayPaymentMethodInfo" + }, + "tokenization_data": { + "$ref": "#/components/schemas/GpayTokenizationData" } } }, - "CreateApiKeyResponse": { + "GpayAllowedMethodsParameters": { "type": "object", - "description": "The response body for creating an API Key.", "required": [ - "key_id", - "merchant_id", - "name", - "api_key", - "created", - "expiration" + "allowed_auth_methods", + "allowed_card_networks" ], "properties": { - "key_id": { + "allowed_auth_methods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of allowed auth methods (ex: 3DS, No3DS, PAN_ONLY etc)" + }, + "allowed_card_networks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of allowed card networks (ex: AMEX,JCB etc)" + } + } + }, + "GpayAllowedPaymentMethods": { + "type": "object", + "required": [ + "type", + "parameters", + "tokenization_specification" + ], + "properties": { + "type": { "type": "string", - "description": "The identifier for the API Key.", - "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", - "maxLength": 64 + "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 identifier for the Merchant Account.", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 64 + "description": "The merchant Identifier that needs to be passed while invoking Gpay SDK", + "nullable": true }, - "name": { + "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", + "description": "The flow in which the code and message occurred for a connector" + }, + "sub_flow": { + "type": "string", + "description": "The sub_flow in which the code and message occurred for a connector" + }, + "code": { + "type": "string", + "description": "code received from the connector" + }, + "message": { "type": "string", - "description": "The unique name for the API Key to help you identify it.", - "example": "Sandbox integration key", - "maxLength": 64 + "description": "message received from the connector" }, - "description": { + "status": { "type": "string", - "description": "The description to provide more context about the API Key.", - "example": "Key used by our developers to integrate with the sandbox environment", - "nullable": true, - "maxLength": 256 + "description": "status provided by the router" }, - "api_key": { + "router_error": { "type": "string", - "description": "The plaintext API Key used for server-side API access. Ensure you store the API Key\nsecurely as you will not be able to see it again.", - "maxLength": 128 + "description": "optional error provided by the router", + "nullable": true }, - "created": { + "decision": { + "$ref": "#/components/schemas/GsmDecision" + }, + "step_up_possible": { + "type": "boolean", + "description": "indicates if step_up retry is possible" + }, + "unified_code": { "type": "string", - "format": "date-time", - "description": "The time at which the API Key was created.", - "example": "2022-09-10T10:11:12Z" + "description": "error code unified across the connectors", + "nullable": true }, - "expiration": { - "$ref": "#/components/schemas/ApiKeyExpiration" - } - } - }, - "CryptoData": { - "type": "object", - "properties": { - "pay_currency": { + "unified_message": { "type": "string", + "description": "error message unified across the connectors", "nullable": true } } }, - "Currency": { + "GsmDecision": { "type": "string", "enum": [ - "AED", - "ALL", - "AMD", - "ANG", - "ARS", - "AUD", - "AWG", - "AZN", - "BBD", - "BDT", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BWP", - "BZD", - "CAD", - "CHF", - "CLP", - "CNY", - "COP", - "CRC", - "CUP", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ETB", - "EUR", - "FJD", - "GBP", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "INR", - "JMD", - "JOD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KRW", - "KWD", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RUB", - "RWF", - "SAR", - "SCR", - "SEK", - "SGD", - "SLL", - "SOS", - "SSP", - "SVC", - "SZL", - "THB", - "TRY", - "TTD", - "TWD", - "TZS", - "UGX", - "USD", - "UYU", - "UZS", - "VND", - "VUV", - "XAF", - "XOF", - "XPF", - "YER", - "ZAR" + "retry", + "requeue", + "do_default" ] }, - "CustomerAcceptance": { + "GsmDeleteRequest": { "type": "object", "required": [ - "acceptance_type" + "connector", + "flow", + "sub_flow", + "code", + "message" ], "properties": { - "acceptance_type": { - "$ref": "#/components/schemas/AcceptanceType" + "connector": { + "type": "string", + "description": "The connector through which payment has gone through" }, - "accepted_at": { + "flow": { "type": "string", - "format": "date-time", - "description": "Specifying when the customer acceptance was provided", - "example": "2022-09-10T10:11:12Z", - "nullable": true + "description": "The flow in which the code and message occurred for a connector" }, - "online": { - "allOf": [ - { - "$ref": "#/components/schemas/OnlineMandate" - } - ], - "nullable": true + "sub_flow": { + "type": "string", + "description": "The sub_flow in which the code and message occurred for a connector" + }, + "code": { + "type": "string", + "description": "code received from the connector" + }, + "message": { + "type": "string", + "description": "message received from the connector" } } }, - "CustomerDeleteResponse": { + "GsmDeleteResponse": { "type": "object", "required": [ - "customer_id", - "customer_deleted", - "address_deleted", - "payment_methods_deleted" + "gsm_rule_delete", + "connector", + "flow", + "sub_flow", + "code" ], "properties": { - "customer_id": { + "gsm_rule_delete": { + "type": "boolean" + }, + "connector": { + "type": "string", + "description": "The connector through which payment has gone through" + }, + "flow": { + "type": "string", + "description": "The flow in which the code and message occurred for a connector" + }, + "sub_flow": { + "type": "string", + "description": "The sub_flow in which the code and message occurred for a connector" + }, + "code": { + "type": "string", + "description": "code received from the connector" + } + } + }, + "GsmResponse": { + "type": "object", + "required": [ + "connector", + "flow", + "sub_flow", + "code", + "message", + "status", + "decision", + "step_up_possible" + ], + "properties": { + "connector": { + "type": "string", + "description": "The connector through which payment has gone through" + }, + "flow": { + "type": "string", + "description": "The flow in which the code and message occurred for a connector" + }, + "sub_flow": { + "type": "string", + "description": "The sub_flow in which the code and message occurred for a connector" + }, + "code": { + "type": "string", + "description": "code received from the connector" + }, + "message": { + "type": "string", + "description": "message received from the connector" + }, + "status": { + "type": "string", + "description": "status provided by the router" + }, + "router_error": { + "type": "string", + "description": "optional error provided by the router", + "nullable": true + }, + "decision": { "type": "string", - "description": "The identifier for the customer object", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "description": "decision to be taken for auto retries flow" }, - "customer_deleted": { + "step_up_possible": { "type": "boolean", - "description": "Whether customer was deleted or not", - "example": false + "description": "indicates if step_up retry is possible" }, - "address_deleted": { - "type": "boolean", - "description": "Whether address was deleted or not", - "example": false + "unified_code": { + "type": "string", + "description": "error code unified across the connectors", + "nullable": true }, - "payment_methods_deleted": { - "type": "boolean", - "description": "Whether payment methods deleted or not", - "example": false + "unified_message": { + "type": "string", + "description": "error message unified across the connectors", + "nullable": true } } }, - "CustomerDetails": { + "GsmRetrieveRequest": { "type": "object", "required": [ - "id" + "connector", + "flow", + "sub_flow", + "code", + "message" ], "properties": { - "id": { - "type": "string", - "description": "The identifier for the customer." + "connector": { + "$ref": "#/components/schemas/Connector" }, - "name": { + "flow": { "type": "string", - "description": "The customer's name", - "example": "John Doe", - "nullable": true, - "maxLength": 255 + "description": "The flow in which the code and message occurred for a connector" }, - "email": { + "sub_flow": { "type": "string", - "description": "The customer's email address", - "example": "johntest@test.com", - "nullable": true, - "maxLength": 255 + "description": "The sub_flow in which the code and message occurred for a connector" }, - "phone": { + "code": { "type": "string", - "description": "The customer's phone number", - "example": "3141592653", - "nullable": true, - "maxLength": 10 + "description": "code received from the connector" }, - "phone_country_code": { + "message": { "type": "string", - "description": "The country code for the customer's phone number", - "example": "+1", - "nullable": true, - "maxLength": 2 + "description": "message received from the connector" } } }, - "CustomerPaymentMethod": { + "GsmUpdateRequest": { "type": "object", "required": [ - "payment_token", - "customer_id", - "payment_method", - "recurring_enabled", - "installment_payment_enabled", - "requires_cvv" + "connector", + "flow", + "sub_flow", + "code", + "message" ], "properties": { - "payment_token": { + "connector": { "type": "string", - "description": "Token for payment method in temporary card locker which gets refreshed often", - "example": "7ebf443f-a050-4067-84e5-e6f6d4800aef" + "description": "The connector through which payment has gone through" }, - "customer_id": { + "flow": { "type": "string", - "description": "The unique identifier of the customer.", - "example": "cus_meowerunwiuwiwqw" + "description": "The flow in which the code and message occurred for a connector" }, - "payment_method": { - "$ref": "#/components/schemas/PaymentMethodType" + "sub_flow": { + "type": "string", + "description": "The sub_flow in which the code and message occurred for a connector" }, - "payment_method_type": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodType" - } - ], - "nullable": true + "code": { + "type": "string", + "description": "code received from the connector" }, - "payment_method_issuer": { + "message": { "type": "string", - "description": "The name of the bank/ provider issuing the payment method to the end user", - "example": "Citibank", - "nullable": true + "description": "message received from the connector" }, - "payment_method_issuer_code": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodIssuerCode" - } - ], + "status": { + "type": "string", + "description": "status provided by the router", "nullable": true }, - "recurring_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method is eligible for recurring payments", - "example": true - }, - "installment_payment_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method is eligible for installment payments", - "example": true - }, - "payment_experience": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentExperience" - }, - "description": "Type of payment experience enabled with the connector", - "example": [ - "redirect_to_url" - ], + "router_error": { + "type": "string", + "description": "optional error provided by the router", "nullable": true }, - "card": { + "decision": { "allOf": [ { - "$ref": "#/components/schemas/CardDetailFromLocker" + "$ref": "#/components/schemas/GsmDecision" } ], "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "step_up_possible": { + "type": "boolean", + "description": "indicates if step_up retry is possible", "nullable": true }, - "created": { + "unified_code": { "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the customer was created", - "example": "2023-01-18T11:04:09.922Z", + "description": "error code unified across the connectors", "nullable": true }, - "bank_transfer": { - "allOf": [ - { - "$ref": "#/components/schemas/Bank" - } - ], + "unified_message": { + "type": "string", + "description": "error message unified across the connectors", "nullable": true - }, - "requires_cvv": { - "type": "boolean", - "description": "Whether this payment method requires CVV to be collected", - "example": true } } }, - "CustomerPaymentMethodsListResponse": { + "IfStatement": { "type": "object", + "description": "Represents an IF statement with conditions and optional nested IF statements\n\n```text\npayment.method = card {\npayment.method.cardtype = (credit, debit) {\npayment.method.network = (amex, rupay, diners)\n}\n}\n```", "required": [ - "customer_payment_methods" + "condition" ], "properties": { - "customer_payment_methods": { + "condition": { "type": "array", "items": { - "$ref": "#/components/schemas/CustomerPaymentMethod" + "$ref": "#/components/schemas/Comparison" + } + }, + "nested": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IfStatement" }, - "description": "List of payment methods for customer" + "nullable": true } } }, - "CustomerRequest": { + "IncrementalAuthorizationResponse": { "type": "object", - "description": "The customer details", + "required": [ + "authorization_id", + "amount", + "status", + "previously_authorized_amount" + ], "properties": { - "customer_id": { + "authorization_id": { "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "description": "The unique identifier of authorization" }, - "name": { + "amount": { + "type": "integer", + "format": "int64", + "description": "Amount the authorization has been made for" + }, + "status": { + "$ref": "#/components/schemas/AuthorizationStatus" + }, + "error_code": { "type": "string", - "description": "The customer's name", - "example": "Jon Test", - "nullable": true, - "maxLength": 255 + "description": "Error code sent by the connector for authorization", + "nullable": true }, - "email": { + "error_message": { "type": "string", - "description": "The customer's email address", - "example": "JonTest@test.com", - "nullable": true, - "maxLength": 255 + "description": "Error message sent by the connector for authorization", + "nullable": true }, - "phone": { + "previously_authorized_amount": { + "type": "integer", + "format": "int64", + "description": "Previously authorized amount for the payment" + } + } + }, + "IndomaretVoucherData": { + "type": "object", + "required": [ + "first_name", + "last_name", + "email" + ], + "properties": { + "first_name": { "type": "string", - "description": "The customer's phone number", - "example": "9999999999", - "nullable": true, - "maxLength": 255 + "description": "The billing first name for Alfamart", + "example": "Jane" }, - "description": { + "last_name": { "type": "string", - "description": "An arbitrary string that you can attach to a customer object.", - "example": "First Customer", - "nullable": true, - "maxLength": 255 + "description": "The billing second name for Alfamart", + "example": "Doe" }, - "phone_country_code": { + "email": { + "type": "string", + "description": "The Email ID for Alfamart", + "example": "example@me.com" + } + } + }, + "IntentStatus": { + "type": "string", + "enum": [ + "succeeded", + "failed", + "cancelled", + "processing", + "requires_customer_action", + "requires_merchant_action", + "requires_payment_method", + "requires_confirmation", + "requires_capture", + "partially_captured", + "partially_captured_and_capturable" + ] + }, + "JCSVoucherData": { + "type": "object", + "required": [ + "first_name", + "last_name", + "email", + "phone_number" + ], + "properties": { + "first_name": { + "type": "string", + "description": "The billing first name for Japanese convenience stores", + "example": "Jane" + }, + "last_name": { "type": "string", - "description": "The country code for the customer phone number", - "example": "+65", - "nullable": true, - "maxLength": 255 + "description": "The billing second name Japanese convenience stores", + "example": "Doe" }, - "address": { - "allOf": [ - { - "$ref": "#/components/schemas/AddressDetails" - } - ], - "nullable": true + "email": { + "type": "string", + "description": "The Email ID for Japanese convenience stores", + "example": "example@me.com" }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", - "nullable": true + "phone_number": { + "type": "string", + "description": "The telephone number for Japanese convenience stores", + "example": "9999999999" } } }, - "CustomerResponse": { + "KakaoPayRedirection": { + "type": "object" + }, + "KlarnaSessionTokenResponse": { "type": "object", "required": [ - "customer_id", - "created_at" + "session_token", + "session_id" ], "properties": { - "customer_id": { + "session_token": { "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "description": "The session token for Klarna" }, - "name": { + "session_id": { "type": "string", - "description": "The customer's name", - "example": "Jon Test", - "nullable": true, - "maxLength": 255 + "description": "The identifier for the session" + } + } + }, + "LinkedRoutingConfigRetrieveResponse": { + "oneOf": [ + { + "$ref": "#/components/schemas/RoutingRetrieveResponse" }, - "email": { - "type": "string", - "description": "The customer's email address", - "example": "JonTest@test.com", - "nullable": true, - "maxLength": 255 + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } + } + ] + }, + "ListBlocklistQuery": { + "type": "object", + "required": [ + "data_kind" + ], + "properties": { + "data_kind": { + "$ref": "#/components/schemas/BlocklistDataKind" }, - "phone": { - "type": "string", - "description": "The customer's phone number", - "example": "9999999999", - "nullable": true, - "maxLength": 255 + "limit": { + "type": "integer", + "format": "int32", + "minimum": 0 }, - "phone_country_code": { - "type": "string", - "description": "The country code for the customer phone number", - "example": "+65", - "nullable": true, - "maxLength": 255 + "offset": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + }, + "MandateAmountData": { + "type": "object", + "required": [ + "amount", + "currency" + ], + "properties": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The maximum amount to be debited for the mandate transaction", + "example": 6540 }, - "description": { - "type": "string", - "description": "An arbitrary string that you can attach to a customer object.", - "example": "First Customer", - "nullable": true, - "maxLength": 255 + "currency": { + "$ref": "#/components/schemas/Currency" }, - "address": { - "allOf": [ - { - "$ref": "#/components/schemas/AddressDetails" - } - ], + "start_date": { + "type": "string", + "format": "date-time", + "description": "Specifying start date of the mandate", + "example": "2022-09-10T00:00:00Z", "nullable": true }, - "created_at": { + "end_date": { "type": "string", "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the customer was created", - "example": "2023-01-18T11:04:09.922Z" + "description": "Specifying end date of the mandate", + "example": "2023-09-10T23:59:59Z", + "nullable": true }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", + "description": "Additional details required by mandate", "nullable": true } } }, - "DisputeResponse": { + "MandateCardDetails": { "type": "object", - "required": [ - "dispute_id", - "payment_id", - "attempt_id", - "amount", - "currency", - "dispute_stage", - "dispute_status", - "connector", - "connector_status", - "connector_dispute_id", - "created_at" - ], "properties": { - "dispute_id": { + "last4_digits": { "type": "string", - "description": "The identifier for dispute" + "description": "The last 4 digits of card", + "nullable": true }, - "payment_id": { + "card_exp_month": { "type": "string", - "description": "The identifier for payment_intent" + "description": "The expiry month of card", + "nullable": true }, - "attempt_id": { + "card_exp_year": { "type": "string", - "description": "The identifier for payment_attempt" + "description": "The expiry year of card", + "nullable": true }, - "amount": { + "card_holder_name": { "type": "string", - "description": "The dispute amount" + "description": "The card holder name", + "nullable": true }, - "currency": { + "card_token": { "type": "string", - "description": "The three-letter ISO currency code" - }, - "dispute_stage": { - "$ref": "#/components/schemas/DisputeStage" - }, - "dispute_status": { - "$ref": "#/components/schemas/DisputeStatus" + "description": "The token from card locker", + "nullable": true }, - "connector": { + "scheme": { "type": "string", - "description": "connector to which dispute is associated with" + "description": "The card scheme network for the particular card", + "nullable": true }, - "connector_status": { + "issuer_country": { "type": "string", - "description": "Status of the dispute sent by connector" + "description": "The country code in in which the card was issued", + "nullable": true }, - "connector_dispute_id": { + "card_fingerprint": { "type": "string", - "description": "Dispute id sent by connector" + "description": "A unique identifier alias to identify a particular card", + "nullable": true }, - "connector_reason": { + "card_isin": { "type": "string", - "description": "Reason of dispute sent by connector", + "description": "The first 6 digits of card", "nullable": true }, - "connector_reason_code": { + "card_issuer": { "type": "string", - "description": "Reason code of dispute sent by connector", + "description": "The bank that issued the card", "nullable": true }, - "challenge_required_by": { - "type": "string", - "format": "date-time", - "description": "Evidence deadline of dispute sent by connector", + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], "nullable": true }, - "connector_created_at": { + "card_type": { "type": "string", - "format": "date-time", - "description": "Dispute created time sent by connector", + "description": "The type of the payment card", "nullable": true - }, - "connector_updated_at": { + } + } + }, + "MandateData": { + "type": "object", + "properties": { + "update_mandate_id": { "type": "string", - "format": "date-time", - "description": "Dispute updated time sent by connector", + "description": "A way to update the mandate's payment method details", "nullable": true }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "Time at which dispute is received" + "customer_acceptance": { + "allOf": [ + { + "$ref": "#/components/schemas/CustomerAcceptance" + } + ], + "nullable": true + }, + "mandate_type": { + "allOf": [ + { + "$ref": "#/components/schemas/MandateType" + } + ], + "nullable": true } } }, - "DisputeResponsePaymentsRetrieve": { + "MandateResponse": { "type": "object", "required": [ - "dispute_id", - "dispute_stage", - "dispute_status", - "connector_status", - "connector_dispute_id", - "created_at" + "mandate_id", + "status", + "payment_method_id", + "payment_method" ], "properties": { - "dispute_id": { + "mandate_id": { "type": "string", - "description": "The identifier for dispute" - }, - "dispute_stage": { - "$ref": "#/components/schemas/DisputeStage" + "description": "The identifier for mandate" }, - "dispute_status": { - "$ref": "#/components/schemas/DisputeStatus" + "status": { + "$ref": "#/components/schemas/MandateStatus" }, - "connector_status": { + "payment_method_id": { "type": "string", - "description": "Status of the dispute sent by connector" + "description": "The identifier for payment method" }, - "connector_dispute_id": { + "payment_method": { "type": "string", - "description": "Dispute id sent by connector" + "description": "The payment method" }, - "connector_reason": { + "payment_method_type": { "type": "string", - "description": "Reason of dispute sent by connector", + "description": "The payment method type", "nullable": true }, - "connector_reason_code": { - "type": "string", - "description": "Reason code of dispute sent by connector", + "card": { + "allOf": [ + { + "$ref": "#/components/schemas/MandateCardDetails" + } + ], "nullable": true }, - "challenge_required_by": { - "type": "string", - "format": "date-time", - "description": "Evidence deadline of dispute sent by connector", + "customer_acceptance": { + "allOf": [ + { + "$ref": "#/components/schemas/CustomerAcceptance" + } + ], "nullable": true - }, - "connector_created_at": { + } + } + }, + "MandateRevokedResponse": { + "type": "object", + "required": [ + "mandate_id", + "status" + ], + "properties": { + "mandate_id": { "type": "string", - "format": "date-time", - "description": "Dispute created time sent by connector", - "nullable": true + "description": "The identifier for mandate" }, - "connector_updated_at": { + "status": { + "$ref": "#/components/schemas/MandateStatus" + }, + "error_code": { "type": "string", - "format": "date-time", - "description": "Dispute updated time sent by connector", + "description": "If there was an error while calling the connectors the code is received here", + "example": "E0001", "nullable": true }, - "created_at": { + "error_message": { "type": "string", - "format": "date-time", - "description": "Time at which dispute is received" + "description": "If there was an error while calling the connector the error message is received here", + "example": "Failed while verifying the card", + "nullable": true } } }, - "DisputeStage": { + "MandateStatus": { "type": "string", + "description": "The status of the mandate, which indicates whether it can be used to initiate a payment.", "enum": [ - "pre_dispute", - "dispute", - "pre_arbitration" + "active", + "inactive", + "pending", + "revoked" ] }, - "DisputeStatus": { - "type": "string", - "enum": [ - "dispute_opened", - "dispute_expired", - "dispute_accepted", - "dispute_cancelled", - "dispute_challenged", - "dispute_won", - "dispute_lost" + "MandateType": { + "oneOf": [ + { + "type": "object", + "required": [ + "single_use" + ], + "properties": { + "single_use": { + "$ref": "#/components/schemas/MandateAmountData" + } + } + }, + { + "type": "object", + "required": [ + "multi_use" + ], + "properties": { + "multi_use": { + "allOf": [ + { + "$ref": "#/components/schemas/MandateAmountData" + } + ], + "nullable": true + } + } + } ] }, - "DokuBankTransferInstructions": { + "MaskedBankDetails": { "type": "object", "required": [ - "expires_at", - "reference", - "instructions_url" + "mask" ], "properties": { - "expires_at": { - "type": "string", - "example": "2023-07-26T17:33:00-07-21" - }, - "reference": { - "type": "string", - "example": "122385736258" - }, - "instructions_url": { + "mask": { "type": "string" } } }, - "DokuBillingDetails": { + "MbWayRedirection": { "type": "object", "required": [ - "first_name", - "last_name", - "email" + "telephone_number" ], "properties": { - "first_name": { - "type": "string", - "description": "The billing first name for Doku", - "example": "Jane" - }, - "last_name": { - "type": "string", - "description": "The billing second name for Doku", - "example": "Doe" - }, - "email": { + "telephone_number": { "type": "string", - "description": "The Email ID for Doku billing", - "example": "example@me.com" + "description": "Telephone number of the shopper. Should be Portuguese phone number." } } }, - "EphemeralKeyCreateResponse": { + "MerchantAccountCreate": { "type": "object", "required": [ - "customer_id", - "created_at", - "expires", - "secret" + "merchant_id" ], "properties": { - "customer_id": { - "type": "string", - "description": "customer_id to which this ephemeral key belongs to" - }, - "created_at": { - "type": "integer", - "format": "int64", - "description": "time at which this ephemeral key was created" - }, - "expires": { - "type": "integer", - "format": "int64", - "description": "time at which this ephemeral key would expire" - }, - "secret": { - "type": "string", - "description": "ephemeral key" - } - } - }, - "EventType": { - "type": "string", - "enum": [ - "payment_succeeded", - "payment_failed", - "payment_processing", - "payment_cancelled", - "action_required", - "refund_succeeded", - "refund_failed", - "dispute_opened", - "dispute_expired", - "dispute_accepted", - "dispute_cancelled", - "dispute_challenged", - "dispute_won", - "dispute_lost", - "mandate_active", - "mandate_revoked" - ] - }, - "FeatureMetadata": { - "type": "object", - "properties": { - "redirect_response": { - "allOf": [ - { - "$ref": "#/components/schemas/RedirectResponse" - } - ], - "nullable": true - } - } - }, - "FieldType": { - "oneOf": [ - { - "type": "string", - "enum": [ - "user_card_number" - ] - }, - { - "type": "string", - "enum": [ - "user_card_expiry_month" - ] - }, - { - "type": "string", - "enum": [ - "user_card_expiry_year" - ] - }, - { - "type": "string", - "enum": [ - "user_card_cvc" - ] - }, - { - "type": "string", - "enum": [ - "user_full_name" - ] - }, - { - "type": "string", - "enum": [ - "user_email_address" - ] - }, - { + "merchant_id": { "type": "string", - "enum": [ - "user_phone_number" - ] + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - { + "merchant_name": { "type": "string", - "enum": [ - "user_country_code" - ] + "description": "Name of the Merchant Account", + "example": "NewAge Retailer", + "nullable": true }, - { - "type": "object", - "required": [ - "user_country" - ], - "properties": { - "user_country": { - "type": "object", - "required": [ - "options" - ], - "properties": { - "options": { - "type": "array", - "items": { - "type": "string" - } - } - } + "merchant_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantDetails" } - } - }, - { - "type": "object", - "required": [ - "user_currency" ], - "properties": { - "user_currency": { - "type": "object", - "required": [ - "options" - ], - "properties": { - "options": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } + "nullable": true }, - { + "return_url": { "type": "string", - "enum": [ - "user_billing_name" - ] + "description": "The URL to redirect after the completion of the operation", + "example": "https://www.example.com/success", + "nullable": true, + "maxLength": 255 }, - { - "type": "string", - "enum": [ - "user_addressline1" - ] + "webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDetails" + } + ], + "nullable": true }, - { - "type": "string", - "enum": [ - "user_addressline2" - ] + "payout_routing_algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + ], + "nullable": true }, - { - "type": "string", - "enum": [ - "user_address_city" - ] + "sub_merchants_enabled": { + "type": "boolean", + "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", + "default": false, + "example": false, + "nullable": true }, - { + "parent_merchant_id": { "type": "string", - "enum": [ - "user_address_pincode" - ] + "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", + "example": "xkkdf909012sdjki2dkh5sdf", + "nullable": true, + "maxLength": 255 }, - { + "enable_payment_response_hash": { + "type": "boolean", + "description": "A boolean value to indicate if payment response hash needs to be enabled", + "default": false, + "example": true, + "nullable": true + }, + "payment_response_hash_key": { "type": "string", - "enum": [ - "user_address_state" - ] + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used.", + "nullable": true }, - { + "redirect_to_merchant_with_http_post": { + "type": "boolean", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "default": false, + "example": true, + "nullable": true + }, + "metadata": { "type": "object", - "required": [ - "user_address_country" - ], - "properties": { - "user_address_country": { - "type": "object", - "required": [ - "options" - ], - "properties": { - "options": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true }, - { + "publishable_key": { "type": "string", - "enum": [ - "user_blik_code" - ] + "description": "API key that will be used for server side API access", + "example": "AH3423bkjbkjdsfbkj", + "nullable": true }, - { + "locker_id": { "type": "string", - "enum": [ - "user_bank" - ] + "description": "An identifier for the vault used to store payment method information.", + "example": "locker_abc123", + "nullable": true }, - { - "type": "string", - "enum": [ - "text" - ] + "primary_business_details": { + "allOf": [ + { + "$ref": "#/components/schemas/PrimaryBusinessDetails" + } + ], + "nullable": true }, - { + "frm_routing_algorithm": { "type": "object", - "required": [ - "drop_down" - ], - "properties": { - "drop_down": { - "type": "object", - "required": [ - "options" - ], - "properties": { - "options": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } + "description": "The frm routing algorithm to be used for routing payments to desired FRM's", + "nullable": true + }, + "organization_id": { + "type": "string", + "description": "The id of the organization to which the merchant belongs to", + "nullable": true } - ], - "description": "Possible field type of required fields in payment_method_data" - }, - "FrmAction": { - "type": "string", - "enum": [ - "cancel_txn", - "auto_refund", - "manual_review" - ] + } }, - "FrmConfigs": { + "MerchantAccountDeleteResponse": { "type": "object", - "description": "Details of FrmConfigs are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", "required": [ - "gateway", - "payment_methods" + "merchant_id", + "deleted" ], "properties": { - "gateway": { - "$ref": "#/components/schemas/ConnectorType" + "merchant_id": { + "type": "string", + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - "payment_methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FrmPaymentMethod" - }, - "description": "payment methods that can be used in the payment" + "deleted": { + "type": "boolean", + "description": "If the connector is deleted or not", + "example": false } } }, - "FrmMessage": { + "MerchantAccountResponse": { "type": "object", - "description": "frm message is an object sent inside the payments response...when frm is invoked, its value is Some(...), else its None", "required": [ - "frm_name" + "merchant_id", + "enable_payment_response_hash", + "redirect_to_merchant_with_http_post", + "primary_business_details", + "organization_id", + "is_recon_enabled", + "recon_status" ], "properties": { - "frm_name": { - "type": "string" + "merchant_id": { + "type": "string", + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - "frm_transaction_id": { + "merchant_name": { "type": "string", + "description": "Name of the Merchant Account", + "example": "NewAge Retailer", "nullable": true }, - "frm_transaction_type": { + "return_url": { "type": "string", - "nullable": true + "description": "The URL to redirect after the completion of the operation", + "example": "https://www.example.com/success", + "nullable": true, + "maxLength": 255 }, - "frm_status": { + "enable_payment_response_hash": { + "type": "boolean", + "description": "A boolean value to indicate if payment response hash needs to be enabled", + "default": false, + "example": true + }, + "payment_response_hash_key": { "type": "string", + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used.", + "example": "xkkdf909012sdjki2dkh5sdf", + "nullable": true, + "maxLength": 255 + }, + "redirect_to_merchant_with_http_post": { + "type": "boolean", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "default": false, + "example": true + }, + "merchant_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantDetails" + } + ], "nullable": true }, - "frm_score": { - "type": "integer", - "format": "int32", + "webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDetails" + } + ], "nullable": true }, - "frm_reason": { + "payout_routing_algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + ], "nullable": true }, - "frm_error": { + "sub_merchants_enabled": { + "type": "boolean", + "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", + "default": false, + "example": false, + "nullable": true + }, + "parent_merchant_id": { + "type": "string", + "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", + "example": "xkkdf909012sdjki2dkh5sdf", + "nullable": true, + "maxLength": 255 + }, + "publishable_key": { "type": "string", + "description": "API key that will be used for server side API access", + "example": "AH3423bkjbkjdsfbkj", "nullable": true - } - } - }, - "FrmPaymentMethod": { - "type": "object", - "description": "Details of FrmPaymentMethod are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", - "required": [ - "payment_method", - "payment_method_types" - ], - "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" }, - "payment_method_types": { + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "locker_id": { + "type": "string", + "description": "An identifier for the vault used to store payment method information.", + "example": "locker_abc123", + "nullable": true + }, + "primary_business_details": { "type": "array", "items": { - "$ref": "#/components/schemas/FrmPaymentMethodType" + "$ref": "#/components/schemas/PrimaryBusinessDetails" }, - "description": "payment method types(credit, debit) that can be used in the payment" - } - } - }, - "FrmPaymentMethodType": { - "type": "object", - "description": "Details of FrmPaymentMethodType are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", - "required": [ - "payment_method_type", - "card_networks", - "flow", - "action" - ], - "properties": { - "payment_method_type": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "card_networks": { - "$ref": "#/components/schemas/CardNetwork" - }, - "flow": { - "$ref": "#/components/schemas/FrmPreferredFlowTypes" + "description": "Details about the primary business unit of the merchant account" }, - "action": { - "$ref": "#/components/schemas/FrmAction" - } - } - }, - "FrmPreferredFlowTypes": { - "type": "string", - "enum": [ - "pre", - "post" - ] - }, - "FutureUsage": { - "type": "string", - "enum": [ - "off_session", - "on_session" - ] - }, - "GcashRedirection": { - "type": "object" - }, - "GiftCardData": { - "oneOf": [ - { - "type": "object", - "required": [ - "givex" - ], - "properties": { - "givex": { - "$ref": "#/components/schemas/GiftCardDetails" + "frm_routing_algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" } - } - }, - { - "type": "object", - "required": [ - "pay_safe_card" ], - "properties": { - "pay_safe_card": { - "type": "object" - } - } - } - ] - }, - "GiftCardDetails": { - "type": "object", - "required": [ - "number", - "cvc" - ], - "properties": { - "number": { + "nullable": true + }, + "intent_fulfillment_time": { + "type": "integer", + "format": "int64", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "nullable": true + }, + "organization_id": { "type": "string", - "description": "The gift card number" + "description": "The organization id merchant is associated with" }, - "cvc": { + "is_recon_enabled": { + "type": "boolean", + "description": "A boolean value to indicate if the merchant has recon service is enabled or not, by default value is false" + }, + "default_profile": { "type": "string", - "description": "The card verification code." + "description": "The default business profile that must be used for creating merchant accounts and payments", + "nullable": true, + "maxLength": 64 + }, + "recon_status": { + "$ref": "#/components/schemas/ReconStatus" } } }, - "GoPayRedirection": { - "type": "object" - }, - "GooglePayPaymentMethodInfo": { + "MerchantAccountUpdate": { "type": "object", "required": [ - "card_network", - "card_details" + "merchant_id" ], "properties": { - "card_network": { + "merchant_id": { "type": "string", - "description": "The name of the card network" + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - "card_details": { + "merchant_name": { "type": "string", - "description": "The details of the card" - } - } - }, - "GooglePayRedirectData": { - "type": "object" - }, - "GooglePaySessionResponse": { - "type": "object", - "required": [ - "merchant_info", - "allowed_payment_methods", - "transaction_info", - "delayed_session_token", - "connector", - "sdk_next_action" - ], - "properties": { - "merchant_info": { - "$ref": "#/components/schemas/GpayMerchantInfo" - }, - "allowed_payment_methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GpayAllowedPaymentMethods" - }, - "description": "List of the allowed payment meythods" - }, - "transaction_info": { - "$ref": "#/components/schemas/GpayTransactionInfo" + "description": "Name of the Merchant Account", + "example": "NewAge Retailer", + "nullable": true }, - "delayed_session_token": { - "type": "boolean", - "description": "Identifier for the delayed session response" + "merchant_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantDetails" + } + ], + "nullable": true }, - "connector": { + "return_url": { "type": "string", - "description": "The name of the connector" + "description": "The URL to redirect after the completion of the operation", + "example": "https://www.example.com/success", + "nullable": true, + "maxLength": 255 }, - "sdk_next_action": { - "$ref": "#/components/schemas/SdkNextAction" + "webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDetails" + } + ], + "nullable": true }, - "secrets": { + "payout_routing_algorithm": { "allOf": [ { - "$ref": "#/components/schemas/SecretInfoToInitiateSdk" + "$ref": "#/components/schemas/RoutingAlgorithm" } ], "nullable": true - } - } - }, - "GooglePayThirdPartySdk": { - "type": "object", - "required": [ - "delayed_session_token", - "connector", - "sdk_next_action" - ], - "properties": { - "delayed_session_token": { + }, + "sub_merchants_enabled": { "type": "boolean", - "description": "Identifier for the delayed session response" + "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", + "default": false, + "example": false, + "nullable": true }, - "connector": { + "parent_merchant_id": { "type": "string", - "description": "The name of the connector" + "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", + "example": "xkkdf909012sdjki2dkh5sdf", + "nullable": true, + "maxLength": 255 }, - "sdk_next_action": { - "$ref": "#/components/schemas/SdkNextAction" - } - } - }, - "GooglePayThirdPartySdkData": { - "type": "object" - }, - "GooglePayWalletData": { - "type": "object", - "required": [ - "type", - "description", - "info", - "tokenization_data" - ], - "properties": { - "type": { + "enable_payment_response_hash": { + "type": "boolean", + "description": "A boolean value to indicate if payment response hash needs to be enabled", + "default": false, + "example": true, + "nullable": true + }, + "payment_response_hash_key": { "type": "string", - "description": "The type of payment method" + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used.", + "nullable": true }, - "description": { + "redirect_to_merchant_with_http_post": { + "type": "boolean", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "default": false, + "example": true, + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "publishable_key": { "type": "string", - "description": "User-facing message to describe the payment method that funds this transaction." + "description": "API key that will be used for server side API access", + "example": "AH3423bkjbkjdsfbkj", + "nullable": true }, - "info": { - "$ref": "#/components/schemas/GooglePayPaymentMethodInfo" + "locker_id": { + "type": "string", + "description": "An identifier for the vault used to store payment method information.", + "example": "locker_abc123", + "nullable": true }, - "tokenization_data": { - "$ref": "#/components/schemas/GpayTokenizationData" + "primary_business_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PrimaryBusinessDetails" + }, + "description": "Details about the primary business unit of the merchant account", + "nullable": true + }, + "frm_routing_algorithm": { + "type": "object", + "description": "The frm routing algorithm to be used for routing payments to desired FRM's", + "nullable": true + }, + "default_profile": { + "type": "string", + "description": "The default business profile that must be used for creating merchant accounts and payments\nTo unset this field, pass an empty string", + "nullable": true, + "maxLength": 64 } } }, - "GpayAllowedMethodsParameters": { + "MerchantConnectorCreate": { "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": [ - "allowed_auth_methods", - "allowed_card_networks" + "connector_type", + "connector_name" ], "properties": { - "allowed_auth_methods": { + "connector_type": { + "$ref": "#/components/schemas/ConnectorType" + }, + "connector_name": { + "$ref": "#/components/schemas/Connector" + }, + "connector_label": { + "type": "string", + "description": "This is an unique label you can generate and pass in order to identify this connector account on your Hyperswitch dashboard and reports. Eg: if your profile label is `default`, connector label can be `stripe_default`", + "example": "stripe_US_travel", + "nullable": true + }, + "profile_id": { + "type": "string", + "description": "Identifier for the business profile, if not provided default will be chosen from merchant account", + "nullable": true + }, + "connector_account_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetails" + } + ], + "nullable": true + }, + "payment_methods_enabled": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/PaymentMethodsEnabled" }, - "description": "The list of allowed auth methods (ex: 3DS, No3DS, PAN_ONLY etc)" + "description": "An object containing the details about the payment methods that need to be enabled under this merchant connector account", + "example": [ + { + "accepted_countries": { + "list": [ + "FR", + "DE", + "IN" + ], + "type": "disable_only" + }, + "accepted_currencies": { + "list": [ + "USD", + "EUR" + ], + "type": "enable_only" + }, + "installment_payment_enabled": true, + "maximum_amount": 68607706, + "minimum_amount": 1, + "payment_method": "wallet", + "payment_method_issuers": [ + "labore magna ipsum", + "aute" + ], + "payment_method_types": [ + "upi_collect", + "upi_intent" + ], + "payment_schemes": [ + "Discover", + "Discover" + ], + "recurring_enabled": true + } + ], + "nullable": true }, - "allowed_card_networks": { + "connector_webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" + } + ], + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "test_mode": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", + "default": false, + "example": false, + "nullable": true + }, + "disabled": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", + "default": false, + "example": false, + "nullable": true + }, + "frm_configs": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/FrmConfigs" }, - "description": "The list of allowed card networks (ex: AMEX,JCB etc)" - } - } - }, - "GpayAllowedPaymentMethods": { - "type": "object", - "required": [ - "type", - "parameters", - "tokenization_specification" - ], - "properties": { - "type": { - "type": "string", - "description": "The type of payment method" - }, - "parameters": { - "$ref": "#/components/schemas/GpayAllowedMethodsParameters" + "description": "Contains the frm configs for the merchant connector", + "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", + "nullable": true }, - "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", + "business_country": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], "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": { + "business_label": { "type": "string", - "description": "The name of the connector" + "description": "The business label to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead", + "nullable": true }, - "gateway_merchant_id": { + "business_sub_label": { "type": "string", - "description": "The merchant ID registered in the connector associated", + "description": "The business sublabel to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead", + "example": "chase", "nullable": true }, - "stripe:version": { + "merchant_connector_id": { "type": "string", + "description": "Unique ID of the connector", + "example": "mca_5apGeP94tMts6rg3U3kR", "nullable": true }, - "stripe:publishableKey": { - "type": "string", + "pm_auth_config": { "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" + "status": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorStatus" + } + ], + "nullable": true } } }, - "GpayTokenizationSpecification": { + "MerchantConnectorDeleteResponse": { "type": "object", "required": [ - "type", - "parameters" + "merchant_id", + "merchant_connector_id", + "deleted" ], "properties": { - "type": { + "merchant_id": { "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" + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - "total_price_status": { + "merchant_connector_id": { "type": "string", - "description": "The total price status (ex: 'FINAL')" + "description": "Unique ID of the connector", + "example": "mca_5apGeP94tMts6rg3U3kR" }, - "total_price": { - "type": "string", - "description": "The total price" + "deleted": { + "type": "boolean", + "description": "If the connector is deleted or not", + "example": false } } }, - "GsmCreateRequest": { + "MerchantConnectorDetails": { "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", + "connector_account_details": { + "type": "object", + "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", "nullable": true }, - "decision": { - "$ref": "#/components/schemas/GsmDecision" - }, - "step_up_possible": { - "type": "boolean" - } - } - }, - "GsmDecision": { - "type": "string", - "enum": [ - "retry", - "requeue", - "do_default" - ] - }, - "GsmDeleteRequest": { - "type": "object", - "required": [ - "connector", - "flow", - "sub_flow", - "code", - "message" - ], - "properties": { - "connector": { - "type": "string" - }, - "flow": { - "type": "string" - }, - "sub_flow": { - "type": "string" - }, - "code": { - "type": "string" - }, - "message": { - "type": "string" + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true } } }, - "GsmDeleteResponse": { + "MerchantConnectorDetailsWrap": { "type": "object", "required": [ - "gsm_rule_delete", - "connector", - "flow", - "sub_flow", - "code" + "creds_identifier" ], "properties": { - "gsm_rule_delete": { - "type": "boolean" - }, - "connector": { - "type": "string" - }, - "flow": { - "type": "string" - }, - "sub_flow": { - "type": "string" + "creds_identifier": { + "type": "string", + "description": "Creds Identifier is to uniquely identify the credentials. Do not send any sensitive info in this field. And do not send the string \"null\"." }, - "code": { - "type": "string" + "encoded_data": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetails" + } + ], + "nullable": true } } }, - "GsmResponse": { + "MerchantConnectorId": { "type": "object", "required": [ - "connector", - "flow", - "sub_flow", - "code", - "message", - "status", - "decision", - "step_up_possible" - ], - "properties": { - "connector": { - "type": "string" - }, - "flow": { - "type": "string" - }, - "sub_flow": { - "type": "string" - }, - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "string" - }, - "router_error": { - "type": "string", - "nullable": true - }, - "decision": { + "merchant_id", + "merchant_connector_id" + ], + "properties": { + "merchant_id": { "type": "string" }, - "step_up_possible": { - "type": "boolean" + "merchant_connector_id": { + "type": "string" } } }, - "GsmRetrieveRequest": { + "MerchantConnectorResponse": { "type": "object", + "description": "Response of creating a new Merchant Connector for the merchant account.\"", "required": [ - "connector", - "flow", - "sub_flow", - "code", - "message" + "connector_type", + "connector_name", + "merchant_connector_id", + "status" ], "properties": { - "connector": { + "connector_type": { + "$ref": "#/components/schemas/ConnectorType" + }, + "connector_name": { "$ref": "#/components/schemas/Connector" }, - "flow": { - "type": "string" + "connector_label": { + "type": "string", + "description": "A unique label to identify the connector account created under a business profile", + "example": "stripe_US_travel", + "nullable": true }, - "sub_flow": { - "type": "string" + "merchant_connector_id": { + "type": "string", + "description": "Unique ID of the merchant connector account", + "example": "mca_5apGeP94tMts6rg3U3kR" }, - "code": { - "type": "string" + "profile_id": { + "type": "string", + "description": "Identifier for the business profile, if not provided default will be chosen from merchant account", + "nullable": true, + "maxLength": 64 }, - "message": { - "type": "string" + "connector_account_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetails" + } + ], + "nullable": true + }, + "payment_methods_enabled": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodsEnabled" + }, + "description": "An object containing the details about the payment methods that need to be enabled under this merchant connector account", + "example": [ + { + "accepted_countries": { + "list": [ + "FR", + "DE", + "IN" + ], + "type": "disable_only" + }, + "accepted_currencies": { + "list": [ + "USD", + "EUR" + ], + "type": "enable_only" + }, + "installment_payment_enabled": true, + "maximum_amount": 68607706, + "minimum_amount": 1, + "payment_method": "wallet", + "payment_method_issuers": [ + "labore magna ipsum", + "aute" + ], + "payment_method_types": [ + "upi_collect", + "upi_intent" + ], + "payment_schemes": [ + "Discover", + "Discover" + ], + "recurring_enabled": true + } + ], + "nullable": true + }, + "connector_webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" + } + ], + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "test_mode": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", + "default": false, + "example": false, + "nullable": true + }, + "disabled": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", + "default": false, + "example": false, + "nullable": true + }, + "frm_configs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FrmConfigs" + }, + "description": "Contains the frm configs for the merchant connector", + "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", + "nullable": true + }, + "business_country": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "business_label": { + "type": "string", + "description": "The business label to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead", + "example": "travel", + "nullable": true + }, + "business_sub_label": { + "type": "string", + "description": "The business sublabel to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead", + "example": "chase", + "nullable": true + }, + "applepay_verified_domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "identifier for the verified domains of a particular connector account", + "nullable": true + }, + "pm_auth_config": { + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, - "GsmUpdateRequest": { + "MerchantConnectorUpdate": { "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", - "flow", - "sub_flow", - "code", - "message" + "connector_type", + "status" ], "properties": { - "connector": { - "type": "string" - }, - "flow": { - "type": "string" - }, - "sub_flow": { - "type": "string" - }, - "code": { - "type": "string" - }, - "message": { - "type": "string" + "connector_type": { + "$ref": "#/components/schemas/ConnectorType" }, - "status": { + "connector_label": { "type": "string", + "description": "This is an unique label you can generate and pass in order to identify this connector account on your Hyperswitch dashboard and reports. Eg: if your profile label is `default`, connector label can be `stripe_default`", + "example": "stripe_US_travel", "nullable": true }, - "router_error": { - "type": "string", + "connector_account_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetails" + } + ], "nullable": true }, - "decision": { + "payment_methods_enabled": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodsEnabled" + }, + "description": "An object containing the details about the payment methods that need to be enabled under this merchant connector account", + "example": [ + { + "accepted_countries": { + "list": [ + "FR", + "DE", + "IN" + ], + "type": "disable_only" + }, + "accepted_currencies": { + "list": [ + "USD", + "EUR" + ], + "type": "enable_only" + }, + "installment_payment_enabled": true, + "maximum_amount": 68607706, + "minimum_amount": 1, + "payment_method": "wallet", + "payment_method_issuers": [ + "labore magna ipsum", + "aute" + ], + "payment_method_types": [ + "upi_collect", + "upi_intent" + ], + "payment_schemes": [ + "Discover", + "Discover" + ], + "recurring_enabled": true + } + ], + "nullable": true + }, + "connector_webhook_details": { "allOf": [ { - "$ref": "#/components/schemas/GsmDecision" + "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" } ], "nullable": true }, - "step_up_possible": { - "type": "boolean", + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "test_mode": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", + "default": false, + "example": false, + "nullable": true + }, + "disabled": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", + "default": false, + "example": false, + "nullable": true + }, + "frm_configs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FrmConfigs" + }, + "description": "Contains the frm configs for the merchant connector", + "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", + "nullable": true + }, + "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, - "IndomaretVoucherData": { + "MerchantConnectorWebhookDetails": { "type": "object", "required": [ - "first_name", - "last_name", - "email" + "merchant_secret", + "additional_secret" ], "properties": { - "first_name": { - "type": "string", - "description": "The billing first name for Alfamart", - "example": "Jane" - }, - "last_name": { + "merchant_secret": { "type": "string", - "description": "The billing second name for Alfamart", - "example": "Doe" + "example": "12345678900987654321" }, - "email": { + "additional_secret": { "type": "string", - "description": "The Email ID for Alfamart", - "example": "example@me.com" + "example": "12345678900987654321" } } }, - "IntentStatus": { - "type": "string", - "enum": [ - "succeeded", - "failed", - "cancelled", - "processing", - "requires_customer_action", - "requires_merchant_action", - "requires_payment_method", - "requires_confirmation", - "requires_capture", - "partially_captured" - ] - }, - "JCSVoucherData": { + "MerchantDetails": { "type": "object", - "required": [ - "first_name", - "last_name", - "email", - "phone_number" - ], "properties": { - "first_name": { + "primary_contact_person": { "type": "string", - "description": "The billing first name for Japanese convenience stores", - "example": "Jane" + "description": "The merchant's primary contact name", + "example": "John Doe", + "nullable": true, + "maxLength": 255 }, - "last_name": { + "primary_phone": { "type": "string", - "description": "The billing second name Japanese convenience stores", - "example": "Doe" + "description": "The merchant's primary phone number", + "example": "999999999", + "nullable": true, + "maxLength": 255 }, - "email": { + "primary_email": { "type": "string", - "description": "The Email ID for Japanese convenience stores", - "example": "example@me.com" + "description": "The merchant's primary email address", + "example": "johndoe@test.com", + "nullable": true, + "maxLength": 255 }, - "phone_number": { - "type": "string", - "description": "The telephone number for Japanese convenience stores", - "example": "9999999999" - } - } - }, - "KakaoPayRedirection": { - "type": "object" - }, - "KlarnaSessionTokenResponse": { - "type": "object", - "required": [ - "session_token", - "session_id" - ], - "properties": { - "session_token": { + "secondary_contact_person": { "type": "string", - "description": "The session token for Klarna" + "description": "The merchant's secondary contact name", + "example": "John Doe2", + "nullable": true, + "maxLength": 255 }, - "session_id": { + "secondary_phone": { "type": "string", - "description": "The identifier for the session" - } - } - }, - "MandateAmountData": { - "type": "object", - "required": [ - "amount", - "currency" - ], - "properties": { - "amount": { - "type": "integer", - "format": "int64", - "description": "The maximum amount to be debited for the mandate transaction", - "example": 6540 + "description": "The merchant's secondary phone number", + "example": "999999988", + "nullable": true, + "maxLength": 255 }, - "currency": { - "$ref": "#/components/schemas/Currency" + "secondary_email": { + "type": "string", + "description": "The merchant's secondary email address", + "example": "johndoe2@test.com", + "nullable": true, + "maxLength": 255 }, - "start_date": { + "website": { "type": "string", - "format": "date-time", - "description": "Specifying start date of the mandate", - "example": "2022-09-10T00:00:00Z", - "nullable": true + "description": "The business website of the merchant", + "example": "www.example.com", + "nullable": true, + "maxLength": 255 }, - "end_date": { + "about_business": { "type": "string", - "format": "date-time", - "description": "Specifying end date of the mandate", - "example": "2023-09-10T23:59:59Z", - "nullable": true + "description": "A brief description about merchant's business", + "example": "Online Retail with a wide selection of organic products for North America", + "nullable": true, + "maxLength": 255 }, - "metadata": { - "type": "object", - "description": "Additional details required by mandate", + "address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], "nullable": true } } }, - "MandateCardDetails": { + "MerchantRoutingAlgorithm": { "type": "object", + "description": "Routing Algorithm specific to merchants", + "required": [ + "id", + "name", + "description", + "algorithm", + "created_at", + "modified_at" + ], "properties": { - "last4_digits": { - "type": "string", - "description": "The last 4 digits of card", - "nullable": true - }, - "card_exp_month": { - "type": "string", - "description": "The expiry month of card", - "nullable": true - }, - "card_exp_year": { - "type": "string", - "description": "The expiry year of card", - "nullable": true + "id": { + "type": "string" }, - "card_holder_name": { - "type": "string", - "description": "The card holder name", - "nullable": true + "name": { + "type": "string" }, - "card_token": { - "type": "string", - "description": "The token from card locker", - "nullable": true + "description": { + "type": "string" }, - "scheme": { - "type": "string", - "description": "The card scheme network for the particular card", - "nullable": true + "algorithm": { + "$ref": "#/components/schemas/RoutingAlgorithm" }, - "issuer_country": { - "type": "string", - "description": "The country code in in which the card was issued", - "nullable": true + "created_at": { + "type": "integer", + "format": "int64" }, - "card_fingerprint": { - "type": "string", - "description": "A unique identifier alias to identify a particular card", - "nullable": true + "modified_at": { + "type": "integer", + "format": "int64" } } }, - "MandateData": { + "MetadataValue": { "type": "object", + "required": [ + "key", + "value" + ], "properties": { - "customer_acceptance": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomerAcceptance" - } - ], - "nullable": true + "key": { + "type": "string" }, - "mandate_type": { - "allOf": [ - { - "$ref": "#/components/schemas/MandateType" - } - ], - "nullable": true + "value": { + "type": "string" } } }, - "MandateResponse": { - "type": "object", - "required": [ - "mandate_id", - "status", - "payment_method_id", - "payment_method" - ], - "properties": { - "mandate_id": { - "type": "string", - "description": "The identifier for mandate" - }, - "status": { - "$ref": "#/components/schemas/MandateStatus" - }, - "payment_method_id": { - "type": "string", - "description": "The identifier for payment method" - }, - "payment_method": { + "MobilePayRedirection": { + "type": "object" + }, + "MomoRedirection": { + "type": "object" + }, + "MultibancoBillingDetails": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { "type": "string", - "description": "The payment method" - }, - "card": { - "allOf": [ - { - "$ref": "#/components/schemas/MandateCardDetails" - } - ], - "nullable": true - }, - "customer_acceptance": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomerAcceptance" - } - ], - "nullable": true + "example": "example@me.com" } } }, - "MandateRevokedResponse": { + "MultibancoTransferInstructions": { "type": "object", "required": [ - "mandate_id", - "status" + "reference", + "entity" ], "properties": { - "mandate_id": { + "reference": { "type": "string", - "description": "The identifier for mandate" + "example": "122385736258" }, - "status": { - "$ref": "#/components/schemas/MandateStatus" + "entity": { + "type": "string", + "example": "12345" } } }, - "MandateStatus": { + "NextActionCall": { "type": "string", - "description": "The status of the mandate, which indicates whether it can be used to initiate a payment", "enum": [ - "active", - "inactive", - "pending", - "revoked" + "confirm", + "sync" ] }, - "MandateType": { + "NextActionData": { "oneOf": [ { "type": "object", + "description": "Contains the url for redirection flow", "required": [ - "single_use" + "redirect_to_url", + "type" ], "properties": { - "single_use": { - "$ref": "#/components/schemas/MandateAmountData" + "redirect_to_url": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "redirect_to_url" + ] } } }, { "type": "object", + "description": "Informs the next steps for bank transfer and also contains the charges details (ex: amount received, amount charged etc)", "required": [ - "multi_use" + "bank_transfer_steps_and_charges_details", + "type" ], "properties": { - "multi_use": { + "bank_transfer_steps_and_charges_details": { + "$ref": "#/components/schemas/BankTransferNextStepsData" + }, + "type": { + "type": "string", + "enum": [ + "display_bank_transfer_information" + ] + } + } + }, + { + "type": "object", + "description": "Contains third party sdk session token response", + "required": [ + "type" + ], + "properties": { + "session_token": { "allOf": [ { - "$ref": "#/components/schemas/MandateAmountData" + "$ref": "#/components/schemas/SessionToken" } ], "nullable": true + }, + "type": { + "type": "string", + "enum": [ + "third_party_sdk_session_token" + ] + } + } + }, + { + "type": "object", + "description": "Contains url for Qr code image, this qr code has to be shown in sdk", + "required": [ + "image_data_url", + "qr_code_url", + "type" + ], + "properties": { + "image_data_url": { + "type": "string", + "description": "Hyperswitch generated image data source url" + }, + "display_to_timestamp": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "qr_code_url": { + "type": "string", + "description": "The url for Qr code given by the connector" + }, + "type": { + "type": "string", + "enum": [ + "qr_code_information" + ] + } + } + }, + { + "type": "object", + "description": "Contains the download url and the reference number for transaction", + "required": [ + "voucher_details", + "type" + ], + "properties": { + "voucher_details": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "display_voucher_information" + ] + } + } + }, + { + "type": "object", + "description": "Contains duration for displaying a wait screen, wait screen with timer is displayed by sdk", + "required": [ + "display_from_timestamp", + "type" + ], + "properties": { + "display_from_timestamp": { + "type": "integer" + }, + "display_to_timestamp": { + "type": "integer", + "nullable": true + }, + "type": { + "type": "string", + "enum": [ + "wait_screen_information" + ] } } } - ] + ], + "discriminator": { + "propertyName": "type" + } + }, + "NextActionType": { + "type": "string", + "enum": [ + "redirect_to_url", + "display_qr_code", + "invoke_sdk_client", + "trigger_api", + "display_bank_transfer_information", + "display_wait_screen" + ] + }, + "NoThirdPartySdkSessionResponse": { + "type": "object", + "required": [ + "epoch_timestamp", + "expires_at", + "merchant_session_identifier", + "nonce", + "merchant_identifier", + "domain_name", + "display_name", + "signature", + "operational_analytics_identifier", + "retries", + "psp_id" + ], + "properties": { + "epoch_timestamp": { + "type": "integer", + "format": "int64", + "description": "Timestamp at which session is requested", + "minimum": 0 + }, + "expires_at": { + "type": "integer", + "format": "int64", + "description": "Timestamp at which session expires", + "minimum": 0 + }, + "merchant_session_identifier": { + "type": "string", + "description": "The identifier for the merchant session" + }, + "nonce": { + "type": "string", + "description": "Apple pay generated unique ID (UUID) value" + }, + "merchant_identifier": { + "type": "string", + "description": "The identifier for the merchant" + }, + "domain_name": { + "type": "string", + "description": "The domain name of the merchant which is registered in Apple Pay" + }, + "display_name": { + "type": "string", + "description": "The name to be displayed on Apple Pay button" + }, + "signature": { + "type": "string", + "description": "A string which represents the properties of a payment" + }, + "operational_analytics_identifier": { + "type": "string", + "description": "The identifier for the operational analytics" + }, + "retries": { + "type": "integer", + "format": "int32", + "description": "The number of retries to get the session response", + "minimum": 0 + }, + "psp_id": { + "type": "string", + "description": "The identifier for the connector transaction" + } + } + }, + "NoonData": { + "type": "object", + "properties": { + "order_category": { + "type": "string", + "description": "Information about the order category that merchant wants to specify at connector level. (e.g. In Noon Payments it can take values like \"pay\", \"food\", or any other custom string set by the merchant in Noon's Dashboard)", + "nullable": true + } + } }, - "MbWayRedirection": { + "NumberComparison": { "type": "object", + "description": "Represents a number comparison for \"NumberComparisonArrayValue\"", "required": [ - "telephone_number" + "comparisonType", + "number" ], "properties": { - "telephone_number": { + "comparisonType": { + "$ref": "#/components/schemas/ComparisonType" + }, + "number": { + "type": "integer", + "format": "int64" + } + } + }, + "OnlineMandate": { + "type": "object", + "required": [ + "ip_address", + "user_agent" + ], + "properties": { + "ip_address": { "type": "string", - "description": "Telephone number of the shopper. Should be Portuguese phone number." + "description": "Ip address of the customer machine from which the mandate was created", + "example": "123.32.25.123" + }, + "user_agent": { + "type": "string", + "description": "The user-agent of the customer's browser" } } }, - "MerchantAccountCreate": { + "OrderDetails": { "type": "object", "required": [ - "merchant_id" + "product_name", + "quantity" ], "properties": { - "merchant_id": { + "product_name": { "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "description": "Name of the product that is being purchased", + "example": "shirt", "maxLength": 255 }, - "merchant_name": { - "type": "string", - "description": "Name of the Merchant Account", - "example": "NewAge Retailer", + "quantity": { + "type": "integer", + "format": "int32", + "description": "The quantity of the product to be purchased", + "example": 1, + "minimum": 0 + }, + "requires_shipping": { + "type": "boolean", "nullable": true }, - "merchant_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantDetails" - } - ], + "product_img_link": { + "type": "string", + "description": "The image URL of the product", "nullable": true }, - "return_url": { + "product_id": { "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://www.example.com/success", - "nullable": true, - "maxLength": 255 + "description": "ID of the product that is being purchased", + "nullable": true }, - "webhook_details": { - "allOf": [ - { - "$ref": "#/components/schemas/WebhookDetails" - } - ], + "category": { + "type": "string", + "description": "Category of the product that is being purchased", "nullable": true }, - "routing_algorithm": { - "type": "object", - "description": "The routing algorithm to be used for routing payments to desired connectors", + "brand": { + "type": "string", + "description": "Brand of the product that is being purchased", "nullable": true }, - "payout_routing_algorithm": { + "product_type": { "allOf": [ { - "$ref": "#/components/schemas/RoutingAlgorithm" + "$ref": "#/components/schemas/ProductType" } ], "nullable": true - }, - "sub_merchants_enabled": { - "type": "boolean", - "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", - "default": false, - "example": false, - "nullable": true - }, - "parent_merchant_id": { + } + } + }, + "OrderDetailsWithAmount": { + "type": "object", + "required": [ + "product_name", + "quantity", + "amount" + ], + "properties": { + "product_name": { "type": "string", - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": "xkkdf909012sdjki2dkh5sdf", - "nullable": true, + "description": "Name of the product that is being purchased", + "example": "shirt", "maxLength": 255 }, - "enable_payment_response_hash": { - "type": "boolean", - "description": "A boolean value to indicate if payment response hash needs to be enabled", - "default": false, - "example": true, - "nullable": true + "quantity": { + "type": "integer", + "format": "int32", + "description": "The quantity of the product to be purchased", + "example": 1, + "minimum": 0 }, - "payment_response_hash_key": { - "type": "string", - "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response\nIf the value is not provided, a default value is used", - "nullable": true + "amount": { + "type": "integer", + "format": "int64", + "description": "the amount per quantity of product" }, - "redirect_to_merchant_with_http_post": { + "requires_shipping": { "type": "boolean", - "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", - "default": false, - "example": true, "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, - "publishable_key": { + "product_img_link": { "type": "string", - "description": "API key that will be used for server side API access", - "example": "AH3423bkjbkjdsfbkj", + "description": "The image URL of the product", "nullable": true }, - "locker_id": { + "product_id": { "type": "string", - "description": "An identifier for the vault used to store payment method information.", - "example": "locker_abc123", - "nullable": true - }, - "primary_business_details": { - "allOf": [ - { - "$ref": "#/components/schemas/PrimaryBusinessDetails" - } - ], + "description": "ID of the product that is being purchased", "nullable": true }, - "frm_routing_algorithm": { - "type": "object", - "description": "The frm routing algorithm to be used for routing payments to desired FRM's", + "category": { + "type": "string", + "description": "Category of the product that is being purchased", "nullable": true }, - "intent_fulfillment_time": { - "type": "integer", - "format": "int32", - "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", - "example": 900, - "nullable": true, - "minimum": 0 - }, - "organization_id": { + "brand": { "type": "string", - "description": "The id of the organization to which the merchant belongs to", + "description": "Brand of the product that is being purchased", "nullable": true }, - "payment_link_config": { + "product_type": { "allOf": [ { - "$ref": "#/components/schemas/PaymentLinkConfig" + "$ref": "#/components/schemas/ProductType" } ], "nullable": true } } }, - "MerchantAccountDeleteResponse": { - "type": "object", - "required": [ - "merchant_id", - "deleted" - ], - "properties": { - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 - }, - "deleted": { - "type": "boolean", - "description": "If the connector is deleted or not", - "example": false - } - } - }, - "MerchantAccountResponse": { + "OutgoingWebhook": { "type": "object", "required": [ "merchant_id", - "enable_payment_response_hash", - "redirect_to_merchant_with_http_post", - "primary_business_details", - "organization_id", - "is_recon_enabled", - "recon_status" + "event_id", + "event_type", + "content" ], "properties": { "merchant_id": { "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "description": "The merchant id of the merchant" }, - "merchant_name": { + "event_id": { "type": "string", - "description": "Name of the Merchant Account", - "example": "NewAge Retailer", - "nullable": true + "description": "The unique event id for each webhook" }, - "return_url": { - "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://www.example.com/success", - "nullable": true, - "maxLength": 255 + "event_type": { + "$ref": "#/components/schemas/EventType" }, - "enable_payment_response_hash": { - "type": "boolean", - "description": "A boolean value to indicate if payment response hash needs to be enabled", - "default": false, - "example": true + "content": { + "$ref": "#/components/schemas/OutgoingWebhookContent" }, - "payment_response_hash_key": { + "timestamp": { "type": "string", - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": "xkkdf909012sdjki2dkh5sdf", - "nullable": true, - "maxLength": 255 - }, - "redirect_to_merchant_with_http_post": { - "type": "boolean", - "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", - "default": false, - "example": true - }, - "merchant_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantDetails" + "format": "date-time", + "description": "The time at which webhook was sent" + } + } + }, + "OutgoingWebhookContent": { + "oneOf": [ + { + "type": "object", + "title": "PaymentsResponse", + "required": [ + "type", + "object" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "payment_details" + ] + }, + "object": { + "$ref": "#/components/schemas/PaymentsResponse" } - ], - "nullable": true + } }, - "webhook_details": { - "allOf": [ - { - "$ref": "#/components/schemas/WebhookDetails" - } + { + "type": "object", + "title": "RefundResponse", + "required": [ + "type", + "object" ], - "nullable": true - }, - "routing_algorithm": { - "allOf": [ - { - "$ref": "#/components/schemas/RoutingAlgorithm" + "properties": { + "type": { + "type": "string", + "enum": [ + "refund_details" + ] + }, + "object": { + "$ref": "#/components/schemas/RefundResponse" } + } + }, + { + "type": "object", + "title": "DisputeResponse", + "required": [ + "type", + "object" ], - "nullable": true + "properties": { + "type": { + "type": "string", + "enum": [ + "dispute_details" + ] + }, + "object": { + "$ref": "#/components/schemas/DisputeResponse" + } + } }, - "payout_routing_algorithm": { - "allOf": [ - { - "$ref": "#/components/schemas/RoutingAlgorithm" + { + "type": "object", + "title": "MandateResponse", + "required": [ + "type", + "object" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "mandate_details" + ] + }, + "object": { + "$ref": "#/components/schemas/MandateResponse" } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "PayLaterData": { + "oneOf": [ + { + "type": "object", + "required": [ + "klarna_redirect" ], - "nullable": true - }, - "sub_merchants_enabled": { - "type": "boolean", - "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", - "default": false, - "example": false, - "nullable": true - }, - "parent_merchant_id": { - "type": "string", - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": "xkkdf909012sdjki2dkh5sdf", - "nullable": true, - "maxLength": 255 - }, - "publishable_key": { - "type": "string", - "description": "API key that will be used for server side API access", - "example": "AH3423bkjbkjdsfbkj", - "nullable": true + "properties": { + "klarna_redirect": { + "type": "object", + "description": "For KlarnaRedirect as PayLater Option", + "required": [ + "billing_email", + "billing_country" + ], + "properties": { + "billing_email": { + "type": "string", + "description": "The billing email" + }, + "billing_country": { + "$ref": "#/components/schemas/CountryAlpha2" + } + } + } + } }, - "metadata": { + { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, - "locker_id": { - "type": "string", - "description": "An identifier for the vault used to store payment method information.", - "example": "locker_abc123", - "nullable": true - }, - "primary_business_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PrimaryBusinessDetails" - }, - "description": "Default business details for connector routing" + "required": [ + "klarna_sdk" + ], + "properties": { + "klarna_sdk": { + "type": "object", + "description": "For Klarna Sdk as PayLater Option", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string", + "description": "The token for the sdk workflow" + } + } + } + } }, - "frm_routing_algorithm": { - "allOf": [ - { - "$ref": "#/components/schemas/RoutingAlgorithm" + { + "type": "object", + "required": [ + "affirm_redirect" + ], + "properties": { + "affirm_redirect": { + "type": "object", + "description": "For Affirm redirect as PayLater Option" } + } + }, + { + "type": "object", + "required": [ + "afterpay_clearpay_redirect" ], - "nullable": true + "properties": { + "afterpay_clearpay_redirect": { + "type": "object", + "description": "For AfterpayClearpay redirect as PayLater Option", + "required": [ + "billing_email", + "billing_name" + ], + "properties": { + "billing_email": { + "type": "string", + "description": "The billing email" + }, + "billing_name": { + "type": "string", + "description": "The billing name" + } + } + } + } }, - "intent_fulfillment_time": { - "type": "integer", - "format": "int64", - "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", - "nullable": true + { + "type": "object", + "required": [ + "pay_bright_redirect" + ], + "properties": { + "pay_bright_redirect": { + "type": "object", + "description": "For PayBright Redirect as PayLater Option" + } + } }, - "organization_id": { - "type": "string", - "description": "The organization id merchant is associated with" + { + "type": "object", + "required": [ + "walley_redirect" + ], + "properties": { + "walley_redirect": { + "type": "object", + "description": "For WalleyRedirect as PayLater Option" + } + } }, - "is_recon_enabled": { - "type": "boolean", - "description": "A boolean value to indicate if the merchant has recon service is enabled or not, by default value is false" + { + "type": "object", + "required": [ + "alma_redirect" + ], + "properties": { + "alma_redirect": { + "type": "object", + "description": "For Alma Redirection as PayLater Option" + } + } }, - "default_profile": { + { + "type": "object", + "required": [ + "atome_redirect" + ], + "properties": { + "atome_redirect": { + "type": "object" + } + } + } + ] + }, + "PayPalWalletData": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { "type": "string", - "description": "The default business profile that must be used for creating merchant accounts and payments", - "nullable": true, - "maxLength": 64 - }, - "recon_status": { - "$ref": "#/components/schemas/ReconStatus" - }, - "payment_link_config": { - "nullable": true + "description": "Token generated for the Apple pay" } } }, - "MerchantAccountUpdate": { + "PaymentAttemptResponse": { "type": "object", "required": [ - "merchant_id" + "attempt_id", + "status", + "amount" ], "properties": { - "merchant_id": { + "attempt_id": { "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "description": "Unique identifier for the attempt" }, - "merchant_name": { - "type": "string", - "description": "Name of the Merchant Account", - "example": "NewAge Retailer", - "nullable": true + "status": { + "$ref": "#/components/schemas/AttemptStatus" }, - "merchant_details": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The payment attempt amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.," + }, + "currency": { "allOf": [ { - "$ref": "#/components/schemas/MerchantDetails" + "$ref": "#/components/schemas/Currency" } ], "nullable": true }, - "return_url": { + "connector": { "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://www.example.com/success", - "nullable": true, - "maxLength": 255 - }, - "webhook_details": { - "allOf": [ - { - "$ref": "#/components/schemas/WebhookDetails" - } - ], + "description": "The connector used for the payment", "nullable": true }, - "routing_algorithm": { - "type": "object", - "description": "The routing algorithm to be used for routing payments to desired connectors", + "error_message": { + "type": "string", + "description": "If there was an error while calling the connector the error message is received here", "nullable": true }, - "payout_routing_algorithm": { + "payment_method": { "allOf": [ { - "$ref": "#/components/schemas/RoutingAlgorithm" + "$ref": "#/components/schemas/PaymentMethod" } ], "nullable": true }, - "sub_merchants_enabled": { - "type": "boolean", - "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", - "default": false, - "example": false, - "nullable": true - }, - "parent_merchant_id": { - "type": "string", - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": "xkkdf909012sdjki2dkh5sdf", - "nullable": true, - "maxLength": 255 - }, - "enable_payment_response_hash": { - "type": "boolean", - "description": "A boolean value to indicate if payment response hash needs to be enabled", - "default": false, - "example": true, - "nullable": true - }, - "payment_response_hash_key": { - "type": "string", - "description": "Refers to the hash key used for payment response", - "nullable": true - }, - "redirect_to_merchant_with_http_post": { - "type": "boolean", - "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", - "default": false, - "example": true, - "nullable": true - }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, - "publishable_key": { - "type": "string", - "description": "API key that will be used for server side API access", - "example": "AH3423bkjbkjdsfbkj", - "nullable": true - }, - "locker_id": { - "type": "string", - "description": "An identifier for the vault used to store payment method information.", - "example": "locker_abc123", - "nullable": true - }, - "primary_business_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PrimaryBusinessDetails" - }, - "description": "Default business details for connector routing", - "nullable": true - }, - "frm_routing_algorithm": { - "type": "object", - "description": "The frm routing algorithm to be used for routing payments to desired FRM's", - "nullable": true - }, - "intent_fulfillment_time": { - "type": "integer", - "format": "int32", - "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", - "nullable": true, - "minimum": 0 - }, - "default_profile": { - "type": "string", - "description": "The default business profile that must be used for creating merchant accounts and payments\nTo unset this field, pass an empty string", - "nullable": true, - "maxLength": 64 - }, - "payment_link_config": { - "nullable": true - } - } - }, - "MerchantConnectorCreate": { - "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_name" - ], - "properties": { - "connector_type": { - "$ref": "#/components/schemas/ConnectorType" - }, - "connector_name": { - "$ref": "#/components/schemas/Connector" - }, - "connector_label": { - "type": "string", - "description": "Connector label for a connector, this can serve as a field to identify the connector as per business details", - "example": "stripe_US_travel", - "nullable": true - }, - "merchant_connector_id": { + "connector_transaction_id": { "type": "string", - "description": "Unique ID of the connector", - "example": "mca_5apGeP94tMts6rg3U3kR", - "nullable": true - }, - "connector_account_details": { - "type": "object", - "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", - "nullable": true - }, - "test_mode": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", - "default": false, - "example": false, + "description": "A unique identifier for a payment provided by the connector", "nullable": true }, - "disabled": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", - "default": false, - "example": false, + "capture_method": { + "allOf": [ + { + "$ref": "#/components/schemas/CaptureMethod" + } + ], "nullable": true }, - "payment_methods_enabled": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodsEnabled" - }, - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": [ + "authentication_type": { + "allOf": [ { - "payment_method": "wallet", - "payment_method_types": [ - "upi_collect", - "upi_intent" - ], - "payment_method_issuers": [ - "labore magna ipsum", - "aute" - ], - "payment_schemes": [ - "Discover", - "Discover" - ], - "accepted_currencies": { - "type": "enable_only", - "list": [ - "USD", - "EUR" - ] - }, - "accepted_countries": { - "type": "disable_only", - "list": [ - "FR", - "DE", - "IN" - ] - }, - "minimum_amount": 1, - "maximum_amount": 68607706, - "recurring_enabled": true, - "installment_payment_enabled": true + "$ref": "#/components/schemas/AuthenticationType" } ], + "default": "three_ds", "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "cancellation_reason": { + "type": "string", + "description": "If the payment was cancelled the reason provided here", "nullable": true }, - "frm_configs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FrmConfigs" - }, - "description": "contains the frm configs for the merchant connector", - "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", + "mandate_id": { + "type": "string", + "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", "nullable": true }, - "business_country": { - "allOf": [ - { - "$ref": "#/components/schemas/CountryAlpha2" - } - ], + "error_code": { + "type": "string", + "description": "If there was an error while calling the connectors the code is received here", "nullable": true }, - "business_label": { + "payment_token": { "type": "string", + "description": "Provide a reference to a stored payment method", "nullable": true }, - "business_sub_label": { - "type": "string", - "description": "Business Sub label of the merchant", - "example": "chase", + "connector_metadata": { + "description": "additional data related to some connectors", "nullable": true }, - "connector_webhook_details": { + "payment_experience": { "allOf": [ { - "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" + "$ref": "#/components/schemas/PaymentExperience" } ], "nullable": true }, - "profile_id": { - "type": "string", - "description": "Identifier for the business profile, if not provided default will be chosen from merchant account", + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } + ], "nullable": true }, - "pm_auth_config": { - "nullable": true - } - } - }, - "MerchantConnectorDeleteResponse": { - "type": "object", - "required": [ - "merchant_id", - "merchant_connector_id", - "deleted" - ], - "properties": { - "merchant_id": { + "reference_id": { "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "description": "reference to the payment at connector side", + "example": "993672945374576J", + "nullable": true }, - "merchant_connector_id": { + "unified_code": { "type": "string", - "description": "Unique ID of the connector", - "example": "mca_5apGeP94tMts6rg3U3kR" - }, - "deleted": { - "type": "boolean", - "description": "If the connector is deleted or not", - "example": false - } - } - }, - "MerchantConnectorDetails": { - "type": "object", - "properties": { - "connector_account_details": { - "type": "object", - "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", + "description": "error code unified across the connectors is received here if there was an error while calling connector", "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "unified_message": { + "type": "string", + "description": "error message unified across the connectors is received here if there was an error while calling connector", "nullable": true } } }, - "MerchantConnectorDetailsWrap": { - "type": "object", - "required": [ - "creds_identifier" - ], - "properties": { - "creds_identifier": { - "type": "string", - "description": "Creds Identifier is to uniquely identify the credentials. Do not send any sensitive info in this field. And do not send the string \"null\"." - }, - "encoded_data": { + "PaymentCreatePaymentLinkConfig": { + "allOf": [ + { "allOf": [ { - "$ref": "#/components/schemas/MerchantConnectorDetails" + "$ref": "#/components/schemas/PaymentLinkConfigRequest" } ], "nullable": true + }, + { + "type": "object" } - } + ] }, - "MerchantConnectorId": { - "type": "object", - "required": [ - "merchant_id", - "merchant_connector_id" - ], - "properties": { - "merchant_id": { - "type": "string" + "PaymentExperience": { + "type": "string", + "description": "To indicate the type of payment experience that the customer would go through", + "enum": [ + "redirect_to_url", + "invoke_sdk_client", + "display_qr_code", + "one_click", + "link_wallet", + "invoke_payment_app", + "display_wait_screen" + ] + }, + "PaymentIdType": { + "oneOf": [ + { + "type": "object", + "required": [ + "PaymentIntentId" + ], + "properties": { + "PaymentIntentId": { + "type": "string", + "description": "The identifier for payment intent" + } + } }, - "merchant_connector_id": { - "type": "string" + { + "type": "object", + "required": [ + "ConnectorTransactionId" + ], + "properties": { + "ConnectorTransactionId": { + "type": "string", + "description": "The identifier for connector transaction" + } + } + }, + { + "type": "object", + "required": [ + "PaymentAttemptId" + ], + "properties": { + "PaymentAttemptId": { + "type": "string", + "description": "The identifier for payment attempt" + } + } + }, + { + "type": "object", + "required": [ + "PreprocessingId" + ], + "properties": { + "PreprocessingId": { + "type": "string", + "description": "The identifier for preprocessing step" + } + } } - } + ] }, - "MerchantConnectorResponse": { + "PaymentLinkConfig": { "type": "object", - "description": "Response of creating a new Merchant Connector for the merchant account.\"", "required": [ - "connector_type", - "connector_name", - "merchant_connector_id" + "theme", + "logo", + "seller_name", + "sdk_layout" ], "properties": { - "connector_type": { - "$ref": "#/components/schemas/ConnectorType" - }, - "connector_name": { - "type": "string", - "description": "Name of the Connector", - "example": "stripe" - }, - "connector_label": { + "theme": { "type": "string", - "description": "Connector label for a connector, this can serve as a field to identify the connector as per business details", - "example": "stripe_US_travel", - "nullable": true + "description": "custom theme for the payment link" }, - "merchant_connector_id": { + "logo": { "type": "string", - "description": "Unique ID of the connector", - "example": "mca_5apGeP94tMts6rg3U3kR" - }, - "connector_account_details": { - "type": "object", - "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", - "nullable": true - }, - "test_mode": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", - "default": false, - "example": false, - "nullable": true - }, - "disabled": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", - "default": false, - "example": false, - "nullable": true - }, - "payment_methods_enabled": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodsEnabled" - }, - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": [ - { - "payment_method": "wallet", - "payment_method_types": [ - "upi_collect", - "upi_intent" - ], - "payment_method_issuers": [ - "labore magna ipsum", - "aute" - ], - "payment_schemes": [ - "Discover", - "Discover" - ], - "accepted_currencies": { - "type": "enable_only", - "list": [ - "USD", - "EUR" - ] - }, - "accepted_countries": { - "type": "disable_only", - "list": [ - "FR", - "DE", - "IN" - ] - }, - "minimum_amount": 1, - "maximum_amount": 68607706, - "recurring_enabled": true, - "installment_payment_enabled": true - } - ], - "nullable": true - }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, - "business_country": { - "allOf": [ - { - "$ref": "#/components/schemas/CountryAlpha2" - } - ], - "nullable": true + "description": "merchant display logo" }, - "business_label": { + "seller_name": { "type": "string", - "description": "Business Type of the merchant", - "example": "travel", - "nullable": true + "description": "Custom merchant name for payment link" }, - "business_sub_label": { + "sdk_layout": { "type": "string", - "description": "Business Sub label of the merchant", - "example": "chase", - "nullable": true - }, - "frm_configs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FrmConfigs" - }, - "description": "contains the frm configs for the merchant connector", - "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", - "nullable": true + "description": "Custom layout for sdk" + } + } + }, + "PaymentLinkConfigRequest": { + "type": "object", + "properties": { + "theme": { + "type": "string", + "description": "custom theme for the payment link", + "example": "#4E6ADD", + "nullable": true, + "maxLength": 255 }, - "connector_webhook_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" - } - ], - "nullable": true + "logo": { + "type": "string", + "description": "merchant display logo", + "example": "https://i.pinimg.com/736x/4d/83/5c/4d835ca8aafbbb15f84d07d926fda473.jpg", + "nullable": true, + "maxLength": 255 }, - "profile_id": { + "seller_name": { "type": "string", - "description": "The business profile this connector must be created in\ndefault value from merchant account is taken if not passed", + "description": "Custom merchant name for payment link", + "example": "hyperswitch", "nullable": true, - "maxLength": 64 + "maxLength": 255 }, - "applepay_verified_domains": { - "type": "array", - "items": { - "type": "string" - }, - "description": "identifier for the verified domains of a particular connector account", - "nullable": true + "sdk_layout": { + "type": "string", + "description": "Custom layout for sdk", + "example": "accordion", + "nullable": true, + "maxLength": 255 + } + } + }, + "PaymentLinkInitiateRequest": { + "type": "object", + "required": [ + "merchant_id", + "payment_id" + ], + "properties": { + "merchant_id": { + "type": "string" }, - "pm_auth_config": { - "nullable": true + "payment_id": { + "type": "string" } } }, - "MerchantConnectorUpdate": { + "PaymentLinkResponse": { "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" + "link", + "payment_link_id" ], "properties": { - "connector_type": { - "$ref": "#/components/schemas/ConnectorType" + "link": { + "type": "string" }, - "connector_label": { + "payment_link_id": { + "type": "string" + } + } + }, + "PaymentLinkStatus": { + "type": "string", + "enum": [ + "active", + "expired" + ] + }, + "PaymentListConstraints": { + "type": "object", + "properties": { + "customer_id": { "type": "string", - "description": "Connector label for a connector, this can serve as a field to identify the connector as per business details", + "description": "The identifier for customer", + "example": "cus_meowuwunwiuwiwqw", "nullable": true }, - "connector_account_details": { - "type": "object", - "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", + "starting_after": { + "type": "string", + "description": "A cursor for use in pagination, fetch the next list after some object", + "example": "pay_fafa124123", "nullable": true }, - "test_mode": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", - "default": false, - "example": false, + "ending_before": { + "type": "string", + "description": "A cursor for use in pagination, fetch the previous list before some object", + "example": "pay_fafa124123", "nullable": true }, - "disabled": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", - "default": false, - "example": false, - "nullable": true + "limit": { + "type": "integer", + "format": "int32", + "description": "limit on the number of objects to return", + "default": 10, + "maximum": 100, + "minimum": 0 }, - "payment_methods_enabled": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodsEnabled" - }, - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": [ - { - "payment_method": "wallet", - "payment_method_types": [ - "upi_collect", - "upi_intent" - ], - "payment_method_issuers": [ - "labore magna ipsum", - "aute" - ], - "payment_schemes": [ - "Discover", - "Discover" - ], - "accepted_currencies": { - "type": "enable_only", - "list": [ - "USD", - "EUR" - ] - }, - "accepted_countries": { - "type": "disable_only", - "list": [ - "FR", - "DE", - "IN" - ] - }, - "minimum_amount": 1, - "maximum_amount": 68607706, - "recurring_enabled": true, - "installment_payment_enabled": true - } - ], + "created": { + "type": "string", + "format": "date-time", + "description": "The time at which payment is created", + "example": "2022-09-10T10:11:12Z", "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "created.lt": { + "type": "string", + "format": "date-time", + "description": "Time less than the payment created time", + "example": "2022-09-10T10:11:12Z", "nullable": true }, - "frm_configs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FrmConfigs" - }, - "description": "contains the frm configs for the merchant connector", - "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", + "created.gt": { + "type": "string", + "format": "date-time", + "description": "Time greater than the payment created time", + "example": "2022-09-10T10:11:12Z", "nullable": true }, - "connector_webhook_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" - } - ], + "created.lte": { + "type": "string", + "format": "date-time", + "description": "Time less than or equals to the payment created time", + "example": "2022-09-10T10:11:12Z", "nullable": true }, - "pm_auth_config": { + "created.gte": { + "type": "string", + "format": "date-time", + "description": "Time greater than or equals to the payment created time", + "example": "2022-09-10T10:11:12Z", "nullable": true } } }, - "MerchantConnectorWebhookDetails": { + "PaymentListResponse": { "type": "object", "required": [ - "merchant_secret", - "additional_secret" + "size", + "data" ], "properties": { - "merchant_secret": { - "type": "string", - "example": "12345678900987654321" + "size": { + "type": "integer", + "description": "The number of payments included in the list", + "minimum": 0 }, - "additional_secret": { - "type": "string", - "example": "12345678900987654321" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentsResponse" + } } } }, - "MerchantDetails": { + "PaymentMethod": { + "type": "string", + "description": "Indicates the type of payment method. Eg: 'card', 'wallet', etc.", + "enum": [ + "card", + "card_redirect", + "pay_later", + "wallet", + "bank_redirect", + "bank_transfer", + "crypto", + "bank_debit", + "reward", + "upi", + "voucher", + "gift_card" + ] + }, + "PaymentMethodCreate": { "type": "object", + "required": [ + "payment_method" + ], "properties": { - "primary_contact_person": { - "type": "string", - "description": "The merchant's primary contact name", - "example": "John Doe", - "nullable": true, - "maxLength": 255 - }, - "primary_phone": { - "type": "string", - "description": "The merchant's primary phone number", - "example": "999999999", - "nullable": true, - "maxLength": 255 - }, - "primary_email": { - "type": "string", - "description": "The merchant's primary email address", - "example": "johndoe@test.com", - "nullable": true, - "maxLength": 255 - }, - "secondary_contact_person": { - "type": "string", - "description": "The merchant's secondary contact name", - "example": "John Doe2", - "nullable": true, - "maxLength": 255 - }, - "secondary_phone": { - "type": "string", - "description": "The merchant's secondary phone number", - "example": "999999988", - "nullable": true, - "maxLength": 255 + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" }, - "secondary_email": { - "type": "string", - "description": "The merchant's secondary email address", - "example": "johndoe2@test.com", - "nullable": true, - "maxLength": 255 + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } + ], + "nullable": true }, - "website": { + "payment_method_issuer": { "type": "string", - "description": "The business website of the merchant", - "example": "www.example.com", - "nullable": true, - "maxLength": 255 + "description": "The name of the bank/ provider issuing the payment method to the end user", + "example": "Citibank", + "nullable": true }, - "about_business": { - "type": "string", - "description": "A brief description about merchant's business", - "example": "Online Retail with a wide selection of organic products for North America", - "nullable": true, - "maxLength": 255 + "payment_method_issuer_code": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodIssuerCode" + } + ], + "nullable": true }, - "address": { + "card": { "allOf": [ { - "$ref": "#/components/schemas/AddressDetails" + "$ref": "#/components/schemas/CardDetail" } ], "nullable": true - } - } - }, - "MobilePayRedirection": { - "type": "object" - }, - "MomoRedirection": { - "type": "object" - }, - "MultibancoBillingDetails": { - "type": "object", - "required": [ - "email" - ], - "properties": { - "email": { - "type": "string", - "example": "example@me.com" - } - } - }, - "MultibancoTransferInstructions": { - "type": "object", - "required": [ - "reference", - "entity" - ], - "properties": { - "reference": { + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "customer_id": { "type": "string", - "example": "122385736258" + "description": "The unique identifier of the customer.", + "example": "cus_meowerunwiuwiwqw", + "nullable": true }, - "entity": { + "card_network": { "type": "string", - "example": "12345" + "description": "The card network", + "example": "Visa", + "nullable": true + }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" + } + ], + "nullable": true } } }, - "NextActionCall": { - "type": "string", - "enum": [ - "confirm", - "sync" - ] - }, - "NextActionData": { + "PaymentMethodData": { "oneOf": [ { "type": "object", - "description": "Contains the url for redirection flow", + "title": "Card", "required": [ - "redirect_to_url", - "type" + "card" ], "properties": { - "redirect_to_url": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "redirect_to_url" - ] + "card": { + "$ref": "#/components/schemas/Card" } } }, { "type": "object", - "description": "Informs the next steps for bank transfer and also contains the charges details (ex: amount received, amount charged etc)", + "title": "CardRedirect", "required": [ - "bank_transfer_steps_and_charges_details", - "type" + "card_redirect" ], "properties": { - "bank_transfer_steps_and_charges_details": { - "$ref": "#/components/schemas/BankTransferNextStepsData" - }, - "type": { - "type": "string", - "enum": [ - "display_bank_transfer_information" - ] + "card_redirect": { + "$ref": "#/components/schemas/CardRedirectData" } } }, { "type": "object", - "description": "Contains third party sdk session token response", + "title": "Wallet", "required": [ - "type" + "wallet" ], "properties": { - "session_token": { - "allOf": [ - { - "$ref": "#/components/schemas/SessionToken" - } - ], - "nullable": true - }, - "type": { - "type": "string", - "enum": [ - "third_party_sdk_session_token" - ] + "wallet": { + "$ref": "#/components/schemas/WalletData" } } }, { "type": "object", - "description": "Contains url for Qr code image, this qr code has to be shown in sdk", + "title": "PayLater", "required": [ - "image_data_url", - "type" + "pay_later" ], "properties": { - "image_data_url": { - "type": "string" - }, - "display_to_timestamp": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "type": { - "type": "string", - "enum": [ - "qr_code_information" - ] + "pay_later": { + "$ref": "#/components/schemas/PayLaterData" } } }, { "type": "object", - "description": "Contains the download url and the reference number for transaction", + "title": "BankRedirect", "required": [ - "voucher_details", - "type" + "bank_redirect" ], "properties": { - "voucher_details": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "display_voucher_information" - ] + "bank_redirect": { + "$ref": "#/components/schemas/BankRedirectData" + } + } + }, + { + "type": "object", + "title": "BankDebit", + "required": [ + "bank_debit" + ], + "properties": { + "bank_debit": { + "$ref": "#/components/schemas/BankDebitData" + } + } + }, + { + "type": "object", + "title": "BankTransfer", + "required": [ + "bank_transfer" + ], + "properties": { + "bank_transfer": { + "$ref": "#/components/schemas/BankTransferData" + } + } + }, + { + "type": "object", + "title": "Crypto", + "required": [ + "crypto" + ], + "properties": { + "crypto": { + "$ref": "#/components/schemas/CryptoData" + } + } + }, + { + "type": "string", + "title": "MandatePayment", + "enum": [ + "mandate_payment" + ] + }, + { + "type": "string", + "title": "Reward", + "enum": [ + "reward" + ] + }, + { + "type": "object", + "title": "Upi", + "required": [ + "upi" + ], + "properties": { + "upi": { + "$ref": "#/components/schemas/UpiData" + } + } + }, + { + "type": "object", + "title": "Voucher", + "required": [ + "voucher" + ], + "properties": { + "voucher": { + "$ref": "#/components/schemas/VoucherData" + } + } + }, + { + "type": "object", + "title": "GiftCard", + "required": [ + "gift_card" + ], + "properties": { + "gift_card": { + "$ref": "#/components/schemas/GiftCardData" } } }, { "type": "object", - "description": "Contains duration for displaying a wait screen, wait screen with timer is displayed by sdk", + "title": "CardToken", "required": [ - "display_from_timestamp", - "type" + "card_token" ], "properties": { - "display_from_timestamp": { - "type": "integer" - }, - "display_to_timestamp": { - "type": "integer", - "nullable": true - }, - "type": { - "type": "string", - "enum": [ - "wait_screen_information" - ] + "card_token": { + "$ref": "#/components/schemas/CardToken" } } } - ], - "discriminator": { - "propertyName": "type" - } - }, - "NextActionType": { - "type": "string", - "enum": [ - "redirect_to_url", - "display_qr_code", - "invoke_sdk_client", - "trigger_api", - "display_bank_transfer_information", - "display_wait_screen" ] }, - "NoThirdPartySdkSessionResponse": { + "PaymentMethodDeleteResponse": { "type": "object", "required": [ - "epoch_timestamp", - "expires_at", - "merchant_session_identifier", - "nonce", - "merchant_identifier", - "domain_name", - "display_name", - "signature", - "operational_analytics_identifier", - "retries", - "psp_id" + "payment_method_id", + "deleted" ], "properties": { - "epoch_timestamp": { - "type": "integer", - "format": "int64", - "description": "Timestamp at which session is requested", - "minimum": 0 - }, - "expires_at": { - "type": "integer", - "format": "int64", - "description": "Timestamp at which session expires", - "minimum": 0 - }, - "merchant_session_identifier": { - "type": "string", - "description": "The identifier for the merchant session" - }, - "nonce": { - "type": "string", - "description": "Apple pay generated unique ID (UUID) value" - }, - "merchant_identifier": { - "type": "string", - "description": "The identifier for the merchant" - }, - "domain_name": { - "type": "string", - "description": "The domain name of the merchant which is registered in Apple Pay" - }, - "display_name": { - "type": "string", - "description": "The name to be displayed on Apple Pay button" - }, - "signature": { - "type": "string", - "description": "A string which represents the properties of a payment" - }, - "operational_analytics_identifier": { + "payment_method_id": { "type": "string", - "description": "The identifier for the operational analytics" - }, - "retries": { - "type": "integer", - "format": "int32", - "description": "The number of retries to get the session response", - "minimum": 0 + "description": "The unique identifier of the Payment method", + "example": "card_rGK4Vi5iSW70MY7J2mIy" }, - "psp_id": { - "type": "string", - "description": "The identifier for the connector transaction" + "deleted": { + "type": "boolean", + "description": "Whether payment method was deleted or not", + "example": true } } }, - "NoonData": { - "type": "object", - "properties": { - "order_category": { - "type": "string", - "description": "Information about the order category that merchant wants to specify at connector level. (e.g. In Noon Payments it can take values like \"pay\", \"food\", or any other custom string set by the merchant in Noon's Dashboard)", - "nullable": true - } - } + "PaymentMethodIssuerCode": { + "type": "string", + "enum": [ + "jp_hdfc", + "jp_icici", + "jp_googlepay", + "jp_applepay", + "jp_phonepay", + "jp_wechat", + "jp_sofort", + "jp_giropay", + "jp_sepa", + "jp_bacs" + ] }, - "OnlineMandate": { + "PaymentMethodList": { "type": "object", "required": [ - "ip_address", - "user_agent" + "payment_method" ], "properties": { - "ip_address": { - "type": "string", - "description": "Ip address of the customer machine from which the mandate was created", - "example": "123.32.25.123" + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" }, - "user_agent": { - "type": "string", - "description": "The user-agent of the customer's browser" + "payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "description": "This is a sub-category of payment method.", + "example": [ + "credit" + ], + "nullable": true } } }, - "OrderDetails": { + "PaymentMethodListResponse": { "type": "object", "required": [ - "product_name", - "quantity" + "payment_methods", + "mandate_payment", + "show_surcharge_breakup_screen" ], "properties": { - "product_name": { + "redirect_url": { "type": "string", - "description": "Name of the product that is being purchased", - "example": "shirt", - "maxLength": 255 + "description": "Redirect URL of the merchant", + "example": "https://www.google.com", + "nullable": true }, - "quantity": { - "type": "integer", - "format": "int32", - "description": "The quantity of the product to be purchased", - "example": 1, - "minimum": 0 + "payment_methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodList" + }, + "description": "Information about the payment method", + "example": [ + { + "payment_experience": null, + "payment_method": "wallet", + "payment_method_issuers": [ + "labore magna ipsum", + "aute" + ] + } + ] }, - "product_img_link": { + "mandate_payment": { + "$ref": "#/components/schemas/MandateType" + }, + "merchant_name": { "type": "string", - "description": "The image URL of the product", "nullable": true - } - } - }, - "OrderDetailsWithAmount": { - "type": "object", - "required": [ - "product_name", - "quantity", - "amount" - ], - "properties": { - "product_name": { - "type": "string", - "description": "Name of the product that is being purchased", - "example": "shirt", - "maxLength": 255 - }, - "quantity": { - "type": "integer", - "format": "int32", - "description": "The quantity of the product to be purchased", - "example": 1, - "minimum": 0 }, - "amount": { - "type": "integer", - "format": "int64", - "description": "the amount per quantity of product" + "show_surcharge_breakup_screen": { + "type": "boolean", + "description": "flag to indicate if surcharge and tax breakup screen should be shown or not" }, - "product_img_link": { - "type": "string", - "description": "The image URL of the product", + "payment_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentType" + } + ], "nullable": true } } }, - "OutgoingWebhook": { + "PaymentMethodResponse": { "type": "object", "required": [ "merchant_id", - "event_id", - "event_type", - "content" + "payment_method_id", + "payment_method", + "recurring_enabled", + "installment_payment_enabled" ], "properties": { "merchant_id": { "type": "string", - "description": "The merchant id of the merchant" + "description": "Unique identifier for a merchant", + "example": "merchant_1671528864" }, - "event_id": { + "customer_id": { "type": "string", - "description": "The unique event id for each webhook" - }, - "event_type": { - "$ref": "#/components/schemas/EventType" - }, - "content": { - "$ref": "#/components/schemas/OutgoingWebhookContent" + "description": "The unique identifier of the customer.", + "example": "cus_meowerunwiuwiwqw", + "nullable": true }, - "timestamp": { + "payment_method_id": { "type": "string", - "format": "date-time", - "description": "The time at which webhook was sent" - } - } - }, - "OutgoingWebhookContent": { - "oneOf": [ - { - "type": "object", - "required": [ - "type", - "object" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "payment_details" - ] - }, - "object": { - "$ref": "#/components/schemas/PaymentsResponse" - } - } + "description": "The unique identifier of the Payment method", + "example": "card_rGK4Vi5iSW70MY7J2mIy" }, - { - "type": "object", - "required": [ - "type", - "object" + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } ], - "properties": { - "type": { - "type": "string", - "enum": [ - "refund_details" - ] - }, - "object": { - "$ref": "#/components/schemas/RefundResponse" + "nullable": true + }, + "card": { + "allOf": [ + { + "$ref": "#/components/schemas/CardDetailFromLocker" } - } + ], + "nullable": true }, - { - "type": "object", - "required": [ - "type", - "object" + "recurring_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method is eligible for recurring payments", + "example": true + }, + "installment_payment_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method is eligible for installment payments", + "example": true + }, + "payment_experience": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentExperience" + }, + "description": "Type of payment experience enabled with the connector", + "example": [ + "redirect_to_url" ], - "properties": { - "type": { - "type": "string", - "enum": [ - "dispute_details" - ] - }, - "object": { - "$ref": "#/components/schemas/DisputeResponse" - } - } + "nullable": true }, - { + "metadata": { "type": "object", - "required": [ - "type", - "object" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "mandate_details" - ] - }, - "object": { - "$ref": "#/components/schemas/MandateResponse" + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "created": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the customer was created", + "example": "2023-01-18T11:04:09.922Z", + "nullable": true + }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" } - } + ], + "nullable": true } - ], - "discriminator": { - "propertyName": "type" } }, - "PayLaterData": { - "oneOf": [ - { - "type": "object", - "required": [ - "klarna_redirect" - ], - "properties": { - "klarna_redirect": { - "type": "object", - "description": "For KlarnaRedirect as PayLater Option", - "required": [ - "billing_email", - "billing_country" - ], - "properties": { - "billing_email": { - "type": "string", - "description": "The billing email" - }, - "billing_country": { - "$ref": "#/components/schemas/CountryAlpha2" - } - } - } - } - }, - { - "type": "object", - "required": [ - "klarna_sdk" - ], - "properties": { - "klarna_sdk": { - "type": "object", - "description": "For Klarna Sdk as PayLater Option", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "string", - "description": "The token for the sdk workflow" - } - } + "PaymentMethodType": { + "type": "string", + "description": "Indicates the sub type of payment method. Eg: 'google_pay' & 'apple_pay' for wallets.", + "enum": [ + "ach", + "affirm", + "afterpay_clearpay", + "alfamart", + "ali_pay", + "ali_pay_hk", + "alma", + "apple_pay", + "atome", + "bacs", + "bancontact_card", + "becs", + "benefit", + "bizum", + "blik", + "boleto", + "bca_bank_transfer", + "bni_va", + "bri_va", + "card_redirect", + "cimb_va", + "classic", + "credit", + "crypto_currency", + "cashapp", + "dana", + "danamon_va", + "debit", + "efecty", + "eps", + "evoucher", + "giropay", + "givex", + "google_pay", + "go_pay", + "gcash", + "ideal", + "interac", + "indomaret", + "klarna", + "kakao_pay", + "mandiri_va", + "knet", + "mb_way", + "mobile_pay", + "momo", + "momo_atm", + "multibanco", + "online_banking_thailand", + "online_banking_czech_republic", + "online_banking_finland", + "online_banking_fpx", + "online_banking_poland", + "online_banking_slovakia", + "oxxo", + "pago_efectivo", + "permata_bank_transfer", + "open_banking_uk", + "pay_bright", + "paypal", + "pix", + "pay_safe_card", + "przelewy24", + "pse", + "red_compra", + "red_pagos", + "samsung_pay", + "sepa", + "sofort", + "swish", + "touch_n_go", + "trustly", + "twint", + "upi_collect", + "vipps", + "walley", + "we_chat_pay", + "seven_eleven", + "lawson", + "mini_stop", + "family_mart", + "seicomart", + "pay_easy" + ] + }, + "PaymentMethodUpdate": { + "type": "object", + "properties": { + "card": { + "allOf": [ + { + "$ref": "#/components/schemas/CardDetail" } - } - }, - { - "type": "object", - "required": [ - "affirm_redirect" ], - "properties": { - "affirm_redirect": { - "type": "object", - "description": "For Affirm redirect as PayLater Option" - } - } + "nullable": true }, - { - "type": "object", - "required": [ - "afterpay_clearpay_redirect" - ], - "properties": { - "afterpay_clearpay_redirect": { - "type": "object", - "description": "For AfterpayClearpay redirect as PayLater Option", - "required": [ - "billing_email", - "billing_name" - ], - "properties": { - "billing_email": { - "type": "string", - "description": "The billing email" - }, - "billing_name": { - "type": "string", - "description": "The billing name" - } - } + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" } - } - }, - { - "type": "object", - "required": [ - "pay_bright_redirect" ], - "properties": { - "pay_bright_redirect": { - "type": "object", - "description": "For PayBright Redirect as PayLater Option" - } - } + "nullable": true }, - { - "type": "object", - "required": [ - "walley_redirect" - ], - "properties": { - "walley_redirect": { - "type": "object", - "description": "For WalleyRedirect as PayLater Option" + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" } - } - }, - { - "type": "object", - "required": [ - "alma_redirect" ], - "properties": { - "alma_redirect": { - "type": "object", - "description": "For Alma Redirection as PayLater Option" - } - } + "nullable": true }, - { + "metadata": { "type": "object", - "required": [ - "atome_redirect" - ], - "properties": { - "atome_redirect": { - "type": "object" - } - } + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true } - ] + } }, - "PayPalWalletData": { + "PaymentMethodsEnabled": { "type": "object", + "description": "Details of all the payment methods enabled for the connector for the given merchant account", "required": [ - "token" + "payment_method" ], "properties": { - "token": { + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RequestPaymentMethodTypes" + }, + "description": "Subtype of payment method", + "example": [ + "credit" + ], + "nullable": true + } + } + }, + "PaymentRetrieveBody": { + "type": "object", + "properties": { + "merchant_id": { "type": "string", - "description": "Token generated for the Apple pay" + "description": "The identifier for the Merchant Account.", + "nullable": true + }, + "force_sync": { + "type": "boolean", + "description": "Decider to enable or disable the connector call for retrieve request", + "nullable": true + }, + "client_secret": { + "type": "string", + "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK", + "nullable": true + }, + "expand_captures": { + "type": "boolean", + "description": "If enabled provides list of captures linked to latest attempt", + "nullable": true + }, + "expand_attempts": { + "type": "boolean", + "description": "If enabled provides list of attempts linked to payment intent", + "nullable": true } } }, - "PaymentAttemptResponse": { + "PaymentType": { + "type": "string", + "description": "To be used to specify the type of payment. Use 'setup_mandate' in case of zero auth flow.", + "enum": [ + "normal", + "new_mandate", + "setup_mandate", + "recurring_mandate" + ] + }, + "PaymentsCancelRequest": { "type": "object", - "required": [ - "attempt_id", - "status", - "amount" - ], "properties": { - "attempt_id": { + "cancellation_reason": { "type": "string", - "description": "Unique identifier for the attempt" + "description": "The reason for the payment cancel", + "nullable": true }, - "status": { - "$ref": "#/components/schemas/AttemptStatus" + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], + "nullable": true + } + } + }, + "PaymentsCaptureRequest": { + "type": "object", + "properties": { + "merchant_id": { + "type": "string", + "description": "The unique identifier for the merchant", + "nullable": true + }, + "amount_to_capture": { + "type": "integer", + "format": "int64", + "description": "The Amount to be captured/ debited from the user's payment method.", + "nullable": true + }, + "refund_uncaptured_amount": { + "type": "boolean", + "description": "Decider to refund the uncaptured amount", + "nullable": true + }, + "statement_descriptor_suffix": { + "type": "string", + "description": "Provides information about a card payment that customers see on their statements.", + "nullable": true + }, + "statement_descriptor_prefix": { + "type": "string", + "description": "Concatenated with the statement descriptor suffix that’s set on the account to form the complete statement descriptor.", + "nullable": true }, + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], + "nullable": true + } + } + }, + "PaymentsConfirmRequest": { + "type": "object", + "properties": { "amount": { "type": "integer", "format": "int64", - "description": "The payment attempt amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.," + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and ¥100 since ¥ is a zero-decimal currency", + "example": 6540, + "nullable": true, + "minimum": 0 }, "currency": { "allOf": [ @@ -8049,27 +12026,46 @@ ], "nullable": true }, - "connector": { - "type": "string", - "description": "The connector used for the payment", + "amount_to_capture": { + "type": "integer", + "format": "int64", + "description": "The Amount to be captured / debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, the default amount_to_capture will be the payment amount.", + "example": 6540, "nullable": true }, - "error_message": { + "payment_id": { "type": "string", - "description": "If there was an error while calling the connector the error message is received here", - "nullable": true + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "pay_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 }, - "payment_method": { + "merchant_id": { + "type": "string", + "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", + "example": "merchant_1668273825", + "nullable": true, + "maxLength": 255 + }, + "routing": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethod" + "$ref": "#/components/schemas/StraightThroughAlgorithm" } ], "nullable": true }, - "connector_transaction_id": { - "type": "string", - "description": "A unique identifier for a payment provided by the connector", + "connector": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connector" + }, + "description": "This allows to manually select a connector with which the payment can go through", + "example": [ + "stripe", + "adyen" + ], "nullable": true }, "capture_method": { @@ -8089,631 +12085,543 @@ "default": "three_ds", "nullable": true }, - "cancellation_reason": { - "type": "string", - "description": "If the payment was cancelled the reason provided here", + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], "nullable": true }, - "mandate_id": { + "capture_on": { "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", + "example": "2022-09-10T10:11:12Z", "nullable": true }, - "error_code": { + "confirm": { + "type": "boolean", + "description": "Whether to confirm the payment (if applicable)", + "default": false, + "example": true, + "nullable": true + }, + "customer": { + "allOf": [ + { + "$ref": "#/components/schemas/CustomerDetails" + } + ], + "nullable": true + }, + "customer_id": { "type": "string", - "description": "If there was an error while calling the connectors the code is received here", + "description": "The identifier for the customer object. This field will be deprecated soon, use the customer object instead", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "nullable": true, + "maxLength": 255 + }, + "email": { + "type": "string", + "description": "The customer's email address This field will be deprecated soon, use the customer object instead", + "example": "johntest@test.com", + "nullable": true, + "maxLength": 255 + }, + "name": { + "type": "string", + "description": "The customer's name.\nThis field will be deprecated soon, use the customer object instead.", + "example": "John Test", + "nullable": true, + "maxLength": 255 + }, + "phone": { + "type": "string", + "description": "The customer's phone number\nThis field will be deprecated soon, use the customer object instead", + "example": "3141592653", + "nullable": true, + "maxLength": 255 + }, + "phone_country_code": { + "type": "string", + "description": "The country code for the customer phone number\nThis field will be deprecated soon, use the customer object instead", + "example": "+1", + "nullable": true, + "maxLength": 255 + }, + "off_session": { + "type": "boolean", + "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. When making a recurring payment by passing a mandate_id, this parameter is mandatory", + "example": true, "nullable": true }, - "payment_token": { + "description": { "type": "string", - "description": "Provide a reference to a stored payment method", + "description": "A description for the payment", + "example": "It's my first payment request", "nullable": true }, - "connector_metadata": { - "description": "additional data related to some connectors", + "return_url": { + "type": "string", + "description": "The URL to redirect after the completion of the operation", + "example": "https://hyperswitch.io", "nullable": true }, - "payment_experience": { + "setup_future_usage": { "allOf": [ { - "$ref": "#/components/schemas/PaymentExperience" + "$ref": "#/components/schemas/FutureUsage" } ], "nullable": true }, - "payment_method_type": { + "payment_method_data": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/PaymentMethodData" } ], "nullable": true }, - "reference_id": { - "type": "string", - "description": "reference to the payment at connector side", - "example": "993672945374576J", - "nullable": true - } - } - }, - "PaymentExperience": { - "type": "string", - "enum": [ - "redirect_to_url", - "invoke_sdk_client", - "display_qr_code", - "one_click", - "link_wallet", - "invoke_payment_app", - "display_wait_screen" - ] - }, - "PaymentIdType": { - "oneOf": [ - { - "type": "object", - "required": [ - "PaymentIntentId" - ], - "properties": { - "PaymentIntentId": { - "type": "string", - "description": "The identifier for payment intent" - } - } - }, - { - "type": "object", - "required": [ - "ConnectorTransactionId" - ], - "properties": { - "ConnectorTransactionId": { - "type": "string", - "description": "The identifier for connector transaction" - } - } - }, - { - "type": "object", - "required": [ - "PaymentAttemptId" - ], - "properties": { - "PaymentAttemptId": { - "type": "string", - "description": "The identifier for payment attempt" + "payment_method": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethod" } - } - }, - { - "type": "object", - "required": [ - "PreprocessingId" ], - "properties": { - "PreprocessingId": { - "type": "string", - "description": "The identifier for preprocessing step" - } - } - } - ] - }, - "PaymentLinkColorSchema": { - "type": "object", - "properties": { - "background_primary_color": { - "type": "string", "nullable": true }, - "sdk_theme": { + "payment_token": { "type": "string", + "description": "Provide a reference to a stored payment method", + "example": "187282ab-40ef-47a9-9206-5099ba31e432", "nullable": true - } - } - }, - "PaymentLinkConfig": { - "type": "object", - "properties": { - "merchant_logo": { + }, + "card_cvc": { "type": "string", + "description": "This is used along with the payment_token field while collecting during saved card payments. This field will be deprecated soon, use the payment_method_data.card_token object instead", + "deprecated": true, "nullable": true }, - "color_scheme": { + "shipping": { "allOf": [ { - "$ref": "#/components/schemas/PaymentLinkColorSchema" + "$ref": "#/components/schemas/Address" } ], "nullable": true - } - } - }, - "PaymentLinkInitiateRequest": { - "type": "object", - "required": [ - "merchant_id", - "payment_id" - ], - "properties": { - "merchant_id": { - "type": "string" - }, - "payment_id": { - "type": "string" - } - } - }, - "PaymentLinkObject": { - "type": "object", - "properties": { - "link_expiry": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "merchant_custom_domain_name": { - "type": "string", - "nullable": true }, - "custom_merchant_name": { + "statement_descriptor_name": { "type": "string", - "description": "Custom merchant name for payment link", - "nullable": true - } - } - }, - "PaymentLinkResponse": { - "type": "object", - "required": [ - "link", - "payment_link_id" - ], - "properties": { - "link": { - "type": "string" + "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", + "example": "Hyperswitch Router", + "nullable": true, + "maxLength": 255 }, - "payment_link_id": { - "type": "string" - } - } - }, - "PaymentListConstraints": { - "type": "object", - "properties": { - "customer_id": { + "statement_descriptor_suffix": { "type": "string", - "description": "The identifier for customer", - "example": "cus_meowuwunwiuwiwqw", - "nullable": true + "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.", + "example": "Payment for shoes purchase", + "nullable": true, + "maxLength": 255 }, - "starting_after": { - "type": "string", - "description": "A cursor for use in pagination, fetch the next list after some object", - "example": "pay_fafa124123", + "order_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetailsWithAmount" + }, + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, - "ending_before": { + "client_secret": { "type": "string", - "description": "A cursor for use in pagination, fetch the previous list before some object", - "example": "pay_fafa124123", + "description": "It's a token used for client side verification.", + "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", "nullable": true }, - "limit": { - "type": "integer", - "format": "int32", - "description": "limit on the number of objects to return", - "default": 10, - "maximum": 100, - "minimum": 0 - }, - "created": { - "type": "string", - "format": "date-time", - "description": "The time at which payment is created", - "example": "2022-09-10T10:11:12Z", + "mandate_data": { + "allOf": [ + { + "$ref": "#/components/schemas/MandateData" + } + ], "nullable": true }, - "created.lt": { + "mandate_id": { "type": "string", - "format": "date-time", - "description": "Time less than the payment created time", - "example": "2022-09-10T10:11:12Z", + "description": "A unique identifier to link the payment to a mandate. To do Recurring payments after a mandate has been created, pass the mandate_id instead of payment_method_data", + "example": "mandate_iwer89rnjef349dni3", + "nullable": true, + "maxLength": 255 + }, + "browser_info": { + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" + } + ], "nullable": true }, - "created.gt": { - "type": "string", - "format": "date-time", - "description": "Time greater than the payment created time", - "example": "2022-09-10T10:11:12Z", + "payment_experience": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentExperience" + } + ], "nullable": true }, - "created.lte": { - "type": "string", - "format": "date-time", - "description": "Time less than or equals to the payment created time", - "example": "2022-09-10T10:11:12Z", + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } + ], "nullable": true }, - "created.gte": { - "type": "string", - "format": "date-time", - "description": "Time greater than or equals to the payment created time", - "example": "2022-09-10T10:11:12Z", + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], "nullable": true - } - } - }, - "PaymentListResponse": { - "type": "object", - "required": [ - "size", - "data" - ], - "properties": { - "size": { - "type": "integer", - "description": "The number of payments included in the list", - "minimum": 0 }, - "data": { + "allowed_payment_method_types": { "type": "array", "items": { - "$ref": "#/components/schemas/PaymentsResponse" - } - } - } - }, - "PaymentMethod": { - "type": "string", - "enum": [ - "card", - "card_redirect", - "pay_later", - "wallet", - "bank_redirect", - "bank_transfer", - "crypto", - "bank_debit", - "reward", - "upi", - "voucher", - "gift_card" - ] - }, - "PaymentMethodCreate": { - "type": "object", - "required": [ - "payment_method" - ], - "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/PaymentMethodType" + }, + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", + "nullable": true }, - "payment_method_type": { + "retry_action": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/RetryAction" } ], "nullable": true }, - "payment_method_issuer": { - "type": "string", - "description": "The name of the bank/ provider issuing the payment method to the end user", - "example": "Citibank", + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", "nullable": true }, - "payment_method_issuer_code": { + "connector_metadata": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodIssuerCode" + "$ref": "#/components/schemas/ConnectorMetadata" } ], "nullable": true }, - "card": { + "feature_metadata": { "allOf": [ { - "$ref": "#/components/schemas/CardDetail" + "$ref": "#/components/schemas/FeatureMetadata" } ], "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "payment_link": { + "type": "boolean", + "description": "Whether to get the payment link (if applicable)", + "default": false, + "example": true, "nullable": true }, - "customer_id": { - "type": "string", - "description": "The unique identifier of the customer.", - "example": "cus_meowerunwiuwiwqw", + "payment_link_config": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentCreatePaymentLinkConfig" + } + ], "nullable": true }, - "card_network": { - "type": "string", - "description": "The card network", - "example": "Visa", + "payment_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentType" + } + ], + "nullable": true + }, + "request_incremental_authorization": { + "type": "boolean", + "description": "Request for an incremental authorization", + "nullable": true + }, + "session_expiry": { + "type": "integer", + "format": "int32", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", "nullable": true } } }, - "PaymentMethodData": { - "oneOf": [ - { - "type": "object", - "required": [ - "card" - ], - "properties": { - "card": { - "$ref": "#/components/schemas/Card" - } - } + "PaymentsCreateRequest": { + "type": "object", + "required": [ + "amount", + "currency" + ], + "properties": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and ¥100 since ¥ is a zero-decimal currency", + "minimum": 0 }, - { - "type": "object", - "required": [ - "card_redirect" - ], - "properties": { - "card_redirect": { - "$ref": "#/components/schemas/CardRedirectData" - } - } + "currency": { + "$ref": "#/components/schemas/Currency" }, - { - "type": "object", - "required": [ - "wallet" - ], - "properties": { - "wallet": { - "$ref": "#/components/schemas/WalletData" - } - } + "amount_to_capture": { + "type": "integer", + "format": "int64", + "description": "The Amount to be captured / debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, the default amount_to_capture will be the payment amount.", + "example": 6540, + "nullable": true }, - { - "type": "object", - "required": [ - "pay_later" - ], - "properties": { - "pay_later": { - "$ref": "#/components/schemas/PayLaterData" + "payment_id": { + "type": "string", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "pay_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 + }, + "merchant_id": { + "type": "string", + "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", + "example": "merchant_1668273825", + "nullable": true, + "maxLength": 255 + }, + "routing": { + "allOf": [ + { + "$ref": "#/components/schemas/StraightThroughAlgorithm" } - } - }, - { - "type": "object", - "required": [ - "bank_redirect" ], - "properties": { - "bank_redirect": { - "$ref": "#/components/schemas/BankRedirectData" - } - } + "nullable": true }, - { - "type": "object", - "required": [ - "bank_debit" + "connector": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connector" + }, + "description": "This allows to manually select a connector with which the payment can go through", + "example": [ + "stripe", + "adyen" ], - "properties": { - "bank_debit": { - "$ref": "#/components/schemas/BankDebitData" - } - } + "nullable": true }, - { - "type": "object", - "required": [ - "bank_transfer" + "capture_method": { + "allOf": [ + { + "$ref": "#/components/schemas/CaptureMethod" + } ], - "properties": { - "bank_transfer": { - "$ref": "#/components/schemas/BankTransferData" + "nullable": true + }, + "authentication_type": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticationType" } - } + ], + "default": "three_ds", + "nullable": true }, - { - "type": "object", - "required": [ - "crypto" + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } ], - "properties": { - "crypto": { - "$ref": "#/components/schemas/CryptoData" + "nullable": true + }, + "capture_on": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, + "confirm": { + "type": "boolean", + "description": "Whether to confirm the payment (if applicable)", + "default": false, + "example": true, + "nullable": true + }, + "customer": { + "allOf": [ + { + "$ref": "#/components/schemas/CustomerDetails" } - } + ], + "nullable": true }, - { + "customer_id": { "type": "string", - "enum": [ - "mandate_payment" - ] + "description": "The identifier for the customer object. This field will be deprecated soon, use the customer object instead", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "nullable": true, + "maxLength": 255 }, - { + "email": { "type": "string", - "enum": [ - "reward" - ] + "description": "The customer's email address This field will be deprecated soon, use the customer object instead", + "example": "johntest@test.com", + "nullable": true, + "maxLength": 255 }, - { - "type": "object", - "required": [ - "upi" - ], - "properties": { - "upi": { - "$ref": "#/components/schemas/UpiData" - } - } + "name": { + "type": "string", + "description": "The customer's name.\nThis field will be deprecated soon, use the customer object instead.", + "example": "John Test", + "nullable": true, + "maxLength": 255 }, - { - "type": "object", - "required": [ - "voucher" - ], - "properties": { - "voucher": { - "$ref": "#/components/schemas/VoucherData" - } - } + "phone": { + "type": "string", + "description": "The customer's phone number\nThis field will be deprecated soon, use the customer object instead", + "example": "3141592653", + "nullable": true, + "maxLength": 255 }, - { - "type": "object", - "required": [ - "gift_card" - ], - "properties": { - "gift_card": { - "$ref": "#/components/schemas/GiftCardData" - } - } - } - ] - }, - "PaymentMethodDeleteResponse": { - "type": "object", - "required": [ - "payment_method_id", - "deleted" - ], - "properties": { - "payment_method_id": { + "phone_country_code": { "type": "string", - "description": "The unique identifier of the Payment method", - "example": "card_rGK4Vi5iSW70MY7J2mIy" + "description": "The country code for the customer phone number\nThis field will be deprecated soon, use the customer object instead", + "example": "+1", + "nullable": true, + "maxLength": 255 }, - "deleted": { + "off_session": { "type": "boolean", - "description": "Whether payment method was deleted or not", - "example": true - } - } - }, - "PaymentMethodIssuerCode": { - "type": "string", - "enum": [ - "jp_hdfc", - "jp_icici", - "jp_googlepay", - "jp_applepay", - "jp_phonepay", - "jp_wechat", - "jp_sofort", - "jp_giropay", - "jp_sepa", - "jp_bacs" - ] - }, - "PaymentMethodList": { - "type": "object", - "required": [ - "payment_method" - ], - "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" + "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. When making a recurring payment by passing a mandate_id, this parameter is mandatory", + "example": true, + "nullable": true }, - "payment_method_types": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "description": "This is a sub-category of payment method.", - "example": [ - "credit" - ], + "description": { + "type": "string", + "description": "A description for the payment", + "example": "It's my first payment request", "nullable": true - } - } - }, - "PaymentMethodListResponse": { - "type": "object", - "required": [ - "payment_methods", - "mandate_payment", - "show_surcharge_breakup_screen" - ], - "properties": { - "redirect_url": { + }, + "return_url": { "type": "string", - "description": "Redirect URL of the merchant", - "example": "https://www.google.com", + "description": "The URL to redirect after the completion of the operation", + "example": "https://hyperswitch.io", "nullable": true }, - "payment_methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodList" - }, - "description": "Information about the payment method", - "example": [ + "setup_future_usage": { + "allOf": [ { - "payment_method": "wallet", - "payment_experience": null, - "payment_method_issuers": [ - "labore magna ipsum", - "aute" - ] + "$ref": "#/components/schemas/FutureUsage" + } + ], + "nullable": true + }, + "payment_method_data": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodData" + } + ], + "nullable": true + }, + "payment_method": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethod" } - ] - }, - "mandate_payment": { - "$ref": "#/components/schemas/MandateType" + ], + "nullable": true }, - "merchant_name": { + "payment_token": { "type": "string", + "description": "Provide a reference to a stored payment method", + "example": "187282ab-40ef-47a9-9206-5099ba31e432", "nullable": true }, - "show_surcharge_breakup_screen": { - "type": "boolean", - "description": "flag to indicate if surcharge and tax breakup screen should be shown or not" + "card_cvc": { + "type": "string", + "description": "This is used along with the payment_token field while collecting during saved card payments. This field will be deprecated soon, use the payment_method_data.card_token object instead", + "deprecated": true, + "nullable": true }, - "payment_type": { + "shipping": { "allOf": [ { - "$ref": "#/components/schemas/PaymentType" + "$ref": "#/components/schemas/Address" } ], "nullable": true - } - } - }, - "PaymentMethodResponse": { - "type": "object", - "required": [ - "merchant_id", - "payment_method_id", - "payment_method", - "recurring_enabled", - "installment_payment_enabled" - ], - "properties": { - "merchant_id": { + }, + "statement_descriptor_name": { "type": "string", - "description": "Unique identifier for a merchant", - "example": "merchant_1671528864" + "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", + "example": "Hyperswitch Router", + "nullable": true, + "maxLength": 255 }, - "customer_id": { + "statement_descriptor_suffix": { "type": "string", - "description": "The unique identifier of the customer.", - "example": "cus_meowerunwiuwiwqw", + "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.", + "example": "Payment for shoes purchase", + "nullable": true, + "maxLength": 255 + }, + "order_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetailsWithAmount" + }, + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, - "payment_method_id": { + "mandate_data": { + "allOf": [ + { + "$ref": "#/components/schemas/MandateData" + } + ], + "nullable": true + }, + "mandate_id": { "type": "string", - "description": "The unique identifier of the Payment method", - "example": "card_rGK4Vi5iSW70MY7J2mIy" + "description": "A unique identifier to link the payment to a mandate. To do Recurring payments after a mandate has been created, pass the mandate_id instead of payment_method_data", + "example": "mandate_iwer89rnjef349dni3", + "nullable": true, + "maxLength": 255 }, - "payment_method": { - "$ref": "#/components/schemas/PaymentMethodType" + "browser_info": { + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" + } + ], + "nullable": true + }, + "payment_experience": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentExperience" + } + ], + "nullable": true }, "payment_method_type": { "allOf": [ @@ -8723,32 +12631,41 @@ ], "nullable": true }, - "card": { + "business_country": { "allOf": [ { - "$ref": "#/components/schemas/CardDetailFromLocker" + "$ref": "#/components/schemas/CountryAlpha2" } ], "nullable": true }, - "recurring_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method is eligible for recurring payments", - "example": true + "business_label": { + "type": "string", + "description": "Business label of the merchant for this payment.\nTo be deprecated soon. Pass the profile_id instead", + "example": "food", + "nullable": true }, - "installment_payment_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method is eligible for installment payments", - "example": true + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], + "nullable": true }, - "payment_experience": { + "allowed_payment_method_types": { "type": "array", "items": { - "$ref": "#/components/schemas/PaymentExperience" + "$ref": "#/components/schemas/PaymentMethodType" }, - "description": "Type of payment experience enabled with the connector", - "example": [ - "redirect_to_url" + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", + "nullable": true + }, + "retry_action": { + "allOf": [ + { + "$ref": "#/components/schemas/RetryAction" + } ], "nullable": true }, @@ -8757,253 +12674,122 @@ "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", "nullable": true }, - "created": { + "connector_metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorMetadata" + } + ], + "nullable": true + }, + "feature_metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/FeatureMetadata" + } + ], + "nullable": true + }, + "payment_link": { + "type": "boolean", + "description": "Whether to get the payment link (if applicable)", + "default": false, + "example": true, + "nullable": true + }, + "payment_link_config": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentCreatePaymentLinkConfig" + } + ], + "nullable": true + }, + "profile_id": { "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the customer was created", - "example": "2023-01-18T11:04:09.922Z", + "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", "nullable": true - } - } - }, - "PaymentMethodType": { - "type": "string", - "enum": [ - "ach", - "affirm", - "afterpay_clearpay", - "alfamart", - "ali_pay", - "ali_pay_hk", - "alma", - "apple_pay", - "atome", - "bacs", - "bancontact_card", - "becs", - "benefit", - "bizum", - "blik", - "boleto", - "bca_bank_transfer", - "bni_va", - "bri_va", - "card_redirect", - "cimb_va", - "classic", - "credit", - "crypto_currency", - "cashapp", - "dana", - "danamon_va", - "debit", - "efecty", - "eps", - "evoucher", - "giropay", - "givex", - "google_pay", - "go_pay", - "gcash", - "ideal", - "interac", - "indomaret", - "klarna", - "kakao_pay", - "mandiri_va", - "knet", - "mb_way", - "mobile_pay", - "momo", - "momo_atm", - "multibanco", - "online_banking_thailand", - "online_banking_czech_republic", - "online_banking_finland", - "online_banking_fpx", - "online_banking_poland", - "online_banking_slovakia", - "oxxo", - "pago_efectivo", - "permata_bank_transfer", - "open_banking_uk", - "pay_bright", - "paypal", - "pix", - "pay_safe_card", - "przelewy24", - "pse", - "red_compra", - "red_pagos", - "samsung_pay", - "sepa", - "sofort", - "swish", - "touch_n_go", - "trustly", - "twint", - "upi_collect", - "vipps", - "walley", - "we_chat_pay", - "seven_eleven", - "lawson", - "mini_stop", - "family_mart", - "seicomart", - "pay_easy" - ] - }, - "PaymentMethodUpdate": { - "type": "object", - "properties": { - "card": { + }, + "surcharge_details": { "allOf": [ { - "$ref": "#/components/schemas/CardDetail" + "$ref": "#/components/schemas/RequestSurchargeDetails" } ], "nullable": true }, - "card_network": { + "payment_type": { "allOf": [ { - "$ref": "#/components/schemas/CardNetwork" + "$ref": "#/components/schemas/PaymentType" } ], "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - } - } - }, - "PaymentMethodsEnabled": { - "type": "object", - "description": "Details of all the payment methods enabled for the connector for the given merchant account", - "required": [ - "payment_method" - ], - "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" - }, - "payment_method_types": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "description": "Subtype of payment method", - "example": [ - "credit" - ], - "nullable": true - } - } - }, - "PaymentRetrieveBody": { - "type": "object", - "properties": { - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account.", - "nullable": true - }, - "force_sync": { + "request_incremental_authorization": { "type": "boolean", - "description": "Decider to enable or disable the connector call for retrieve request", - "nullable": true - }, - "client_secret": { - "type": "string", - "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK", + "description": "Request for an incremental authorization", "nullable": true }, - "expand_captures": { - "type": "boolean", - "description": "If enabled provides list of captures linked to latest attempt", - "nullable": true + "session_expiry": { + "type": "integer", + "format": "int32", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 }, - "expand_attempts": { - "type": "boolean", - "description": "If enabled provides list of attempts linked to payment intent", + "frm_metadata": { + "description": "additional data related to some frm connectors", "nullable": true } } }, - "PaymentType": { - "type": "string", - "enum": [ - "normal", - "new_mandate", - "setup_mandate", - "recurring_mandate" - ] - }, - "PaymentsCancelRequest": { + "PaymentsIncrementalAuthorizationRequest": { "type": "object", "required": [ - "merchant_connector_details" + "amount" ], "properties": { - "cancellation_reason": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The total amount including previously authorized amount and additional amount", + "example": 6540 + }, + "reason": { "type": "string", - "description": "The reason for the payment cancel", + "description": "Reason for incremental authorization", "nullable": true - }, - "merchant_connector_details": { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" } } }, - "PaymentsCaptureRequest": { + "PaymentsRequest": { "type": "object", "properties": { - "merchant_id": { - "type": "string", - "description": "The unique identifier for the merchant", - "nullable": true - }, - "amount_to_capture": { + "amount": { "type": "integer", "format": "int64", - "description": "The Amount to be captured/ debited from the user's payment method.", - "nullable": true - }, - "refund_uncaptured_amount": { - "type": "boolean", - "description": "Decider to refund the uncaptured amount", - "nullable": true - }, - "statement_descriptor_suffix": { - "type": "string", - "description": "Provides information about a card payment that customers see on their statements.", - "nullable": true - }, - "statement_descriptor_prefix": { - "type": "string", - "description": "Concatenated with the statement descriptor suffix that’s set on the account to form the complete statement descriptor.", - "nullable": true + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and ¥100 since ¥ is a zero-decimal currency", + "example": 6540, + "nullable": true, + "minimum": 0 }, - "merchant_connector_details": { + "currency": { "allOf": [ { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + "$ref": "#/components/schemas/Currency" } ], "nullable": true - } - } - }, - "PaymentsCreateRequest": { - "type": "object", - "required": [ - "amount", - "currency" - ], - "properties": { + }, + "amount_to_capture": { + "type": "integer", + "format": "int64", + "description": "The Amount to be captured / debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, the default amount_to_capture will be the payment amount.", + "example": 6540, + "nullable": true + }, "payment_id": { "type": "string", "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", @@ -9019,18 +12805,10 @@ "nullable": true, "maxLength": 255 }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 6540, - "nullable": true, - "minimum": 0 - }, "routing": { "allOf": [ { - "$ref": "#/components/schemas/RoutingAlgorithm" + "$ref": "#/components/schemas/StraightThroughAlgorithm" } ], "nullable": true @@ -9040,34 +12818,36 @@ "items": { "$ref": "#/components/schemas/Connector" }, - "description": "This allows the merchant to manually select a connector with which the payment can go through", + "description": "This allows to manually select a connector with which the payment can go through", "example": [ "stripe", "adyen" ], "nullable": true }, - "currency": { + "capture_method": { "allOf": [ { - "$ref": "#/components/schemas/Currency" + "$ref": "#/components/schemas/CaptureMethod" } ], "nullable": true }, - "capture_method": { + "authentication_type": { "allOf": [ { - "$ref": "#/components/schemas/CaptureMethod" + "$ref": "#/components/schemas/AuthenticationType" } ], + "default": "three_ds", "nullable": true }, - "amount_to_capture": { - "type": "integer", - "format": "int64", - "description": "The Amount to be captured/ debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,\nIf not provided, the default amount_to_capture will be the payment amount.", - "example": 6540, + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], "nullable": true }, "capture_on": { @@ -9094,21 +12874,21 @@ }, "customer_id": { "type": "string", - "description": "The identifier for the customer object.\nThis field will be deprecated soon, use the customer object instead", + "description": "The identifier for the customer object. This field will be deprecated soon, use the customer object instead", "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", "nullable": true, "maxLength": 255 }, "email": { "type": "string", - "description": "The customer's email address\nThis field will be deprecated soon, use the customer object instead", + "description": "The customer's email address This field will be deprecated soon, use the customer object instead", "example": "johntest@test.com", "nullable": true, "maxLength": 255 }, "name": { "type": "string", - "description": "description: The customer's name\nThis field will be deprecated soon, use the customer object instead", + "description": "The customer's name.\nThis field will be deprecated soon, use the customer object instead.", "example": "John Test", "nullable": true, "maxLength": 255 @@ -9129,13 +12909,13 @@ }, "off_session": { "type": "boolean", - "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with `confirm: true`.", + "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. When making a recurring payment by passing a mandate_id, this parameter is mandatory", "example": true, "nullable": true }, "description": { "type": "string", - "description": "A description of the payment", + "description": "A description for the payment", "example": "It's my first payment request", "nullable": true }, @@ -9153,15 +12933,6 @@ ], "nullable": true }, - "authentication_type": { - "allOf": [ - { - "$ref": "#/components/schemas/AuthenticationType" - } - ], - "default": "three_ds", - "nullable": true - }, "payment_method_data": { "allOf": [ { @@ -9186,7 +12957,8 @@ }, "card_cvc": { "type": "string", - "description": "This is used when payment is to be confirmed and the card is not saved", + "description": "This is used along with the payment_token field while collecting during saved card payments. This field will be deprecated soon, use the payment_method_data.card_token object instead", + "deprecated": true, "nullable": true }, "shipping": { @@ -9197,14 +12969,6 @@ ], "nullable": true }, - "billing": { - "allOf": [ - { - "$ref": "#/components/schemas/Address" - } - ], - "nullable": true - }, "statement_descriptor_name": { "type": "string", "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", @@ -9224,8 +12988,8 @@ "items": { "$ref": "#/components/schemas/OrderDetailsWithAmount" }, - "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", - "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, "client_secret": { @@ -9244,14 +13008,17 @@ }, "mandate_id": { "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", + "description": "A unique identifier to link the payment to a mandate. To do Recurring payments after a mandate has been created, pass the mandate_id instead of payment_method_data", "example": "mandate_iwer89rnjef349dni3", "nullable": true, "maxLength": 255 }, "browser_info": { - "type": "object", - "description": "Additional details required by 3DS 2.0", + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" + } + ], "nullable": true }, "payment_experience": { @@ -9280,7 +13047,7 @@ }, "business_label": { "type": "string", - "description": "Business label of the merchant for this payment", + "description": "Business label of the merchant for this payment.\nTo be deprecated soon. Pass the profile_id instead", "example": "food", "nullable": true }, @@ -9297,7 +13064,7 @@ "items": { "$ref": "#/components/schemas/PaymentMethodType" }, - "description": "Allowed Payment Method Types for a given PaymentIntent", + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", "nullable": true }, "business_sub_label": { @@ -9334,10 +13101,17 @@ ], "nullable": true }, - "payment_link_object": { + "payment_link": { + "type": "boolean", + "description": "Whether to get the payment link (if applicable)", + "default": false, + "example": true, + "nullable": true + }, + "payment_link_config": { "allOf": [ { - "$ref": "#/components/schemas/PaymentLinkObject" + "$ref": "#/components/schemas/PaymentCreatePaymentLinkConfig" } ], "nullable": true @@ -9362,15 +13136,40 @@ } ], "nullable": true + }, + "request_incremental_authorization": { + "type": "boolean", + "description": "Request for an incremental authorization", + "nullable": true + }, + "session_expiry": { + "type": "integer", + "format": "int32", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", + "nullable": true } } }, - "PaymentsRequest": { + "PaymentsResponse": { "type": "object", + "required": [ + "status", + "amount", + "net_amount", + "currency", + "payment_method", + "attempt_count" + ], "properties": { "payment_id": { "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", "example": "pay_mbabizu24mvu3mela5njyhpit4", "nullable": true, "maxLength": 30, @@ -9383,158 +13182,157 @@ "nullable": true, "maxLength": 255 }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 6540, - "nullable": true, - "minimum": 0 - }, - "routing": { - "allOf": [ - { - "$ref": "#/components/schemas/RoutingAlgorithm" - } - ], - "nullable": true - }, - "connector": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Connector" - }, - "description": "This allows the merchant to manually select a connector with which the payment can go through", - "example": [ - "stripe", - "adyen" - ], - "nullable": true - }, - "currency": { - "allOf": [ - { - "$ref": "#/components/schemas/Currency" - } - ], - "nullable": true - }, - "capture_method": { + "status": { "allOf": [ { - "$ref": "#/components/schemas/CaptureMethod" + "$ref": "#/components/schemas/IntentStatus" } ], - "nullable": true + "default": "requires_confirmation" }, - "amount_to_capture": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", + "example": 100 + }, + "net_amount": { + "type": "integer", + "format": "int64", + "description": "The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount,\nIf no surcharge_details, net_amount = amount", + "example": 110 + }, + "amount_capturable": { + "type": "integer", + "format": "int64", + "description": "The maximum amount that could be captured from the payment", + "example": 6540, + "nullable": true, + "minimum": 100 + }, + "amount_received": { "type": "integer", "format": "int64", - "description": "The Amount to be captured/ debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,\nIf not provided, the default amount_to_capture will be the payment amount.", + "description": "The amount which is already captured from the payment", "example": 6540, - "nullable": true + "nullable": true, + "minimum": 100 }, - "capture_on": { + "connector": { "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", - "example": "2022-09-10T10:11:12Z", + "description": "The connector used for the payment", + "example": "stripe", "nullable": true }, - "confirm": { - "type": "boolean", - "description": "Whether to confirm the payment (if applicable)", - "default": false, - "example": true, + "client_secret": { + "type": "string", + "description": "It's a token used for client side verification.", + "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", "nullable": true }, - "customer": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomerDetails" - } - ], + "created": { + "type": "string", + "format": "date-time", + "description": "Time when the payment was created", + "example": "2022-09-10T10:11:12Z", "nullable": true }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, "customer_id": { "type": "string", - "description": "The identifier for the customer object.\nThis field will be deprecated soon, use the customer object instead", + "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", "nullable": true, "maxLength": 255 }, - "email": { - "type": "string", - "description": "The customer's email address\nThis field will be deprecated soon, use the customer object instead", - "example": "johntest@test.com", - "nullable": true, - "maxLength": 255 - }, - "name": { + "description": { "type": "string", - "description": "description: The customer's name\nThis field will be deprecated soon, use the customer object instead", - "example": "John Test", - "nullable": true, - "maxLength": 255 + "description": "A description of the payment", + "example": "It's my first payment request", + "nullable": true }, - "phone": { - "type": "string", - "description": "The customer's phone number\nThis field will be deprecated soon, use the customer object instead", - "example": "3141592653", - "nullable": true, - "maxLength": 255 + "refunds": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RefundResponse" + }, + "description": "List of refund that happened on this intent", + "nullable": true }, - "phone_country_code": { - "type": "string", - "description": "The country code for the customer phone number\nThis field will be deprecated soon, use the customer object instead", - "example": "+1", - "nullable": true, - "maxLength": 255 + "disputes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DisputeResponsePaymentsRetrieve" + }, + "description": "List of dispute that happened on this intent", + "nullable": true }, - "off_session": { - "type": "boolean", - "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with `confirm: true`.", - "example": true, + "attempts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentAttemptResponse" + }, + "description": "List of attempts that happened on this intent", "nullable": true }, - "description": { - "type": "string", - "description": "A description of the payment", - "example": "It's my first payment request", + "captures": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaptureResponse" + }, + "description": "List of captures done on latest attempt", "nullable": true }, - "return_url": { + "mandate_id": { "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://hyperswitch.io", - "nullable": true + "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", + "example": "mandate_iwer89rnjef349dni3", + "nullable": true, + "maxLength": 255 }, - "setup_future_usage": { + "mandate_data": { "allOf": [ { - "$ref": "#/components/schemas/FutureUsage" + "$ref": "#/components/schemas/MandateData" } ], "nullable": true }, - "authentication_type": { + "setup_future_usage": { "allOf": [ { - "$ref": "#/components/schemas/AuthenticationType" + "$ref": "#/components/schemas/FutureUsage" } ], - "default": "three_ds", "nullable": true }, - "payment_method_data": { + "off_session": { + "type": "boolean", + "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with confirm=true.", + "example": true, + "nullable": true + }, + "capture_on": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, + "capture_method": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodData" + "$ref": "#/components/schemas/CaptureMethod" } ], "nullable": true }, "payment_method": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "payment_method_data": { "allOf": [ { "$ref": "#/components/schemas/PaymentMethod" @@ -9548,11 +13346,6 @@ "example": "187282ab-40ef-47a9-9206-5099ba31e432", "nullable": true }, - "card_cvc": { - "type": "string", - "description": "This is used when payment is to be confirmed and the card is not saved", - "nullable": true - }, "shipping": { "allOf": [ { @@ -9569,53 +13362,98 @@ ], "nullable": true }, - "statement_descriptor_name": { + "order_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetailsWithAmount" + }, + "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", + "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]", + "nullable": true + }, + "email": { "type": "string", - "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", - "example": "Hyperswitch Router", + "description": "description: The customer's email address", + "example": "johntest@test.com", "nullable": true, "maxLength": 255 }, - "statement_descriptor_suffix": { + "name": { "type": "string", - "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.", - "example": "Payment for shoes purchase", + "description": "description: The customer's name", + "example": "John Test", "nullable": true, "maxLength": 255 }, - "order_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderDetailsWithAmount" - }, - "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", - "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", - "nullable": true + "phone": { + "type": "string", + "description": "The customer's phone number", + "example": "3141592653", + "nullable": true, + "maxLength": 255 }, - "client_secret": { + "return_url": { "type": "string", - "description": "It's a token used for client side verification.", - "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", + "description": "The URL to redirect after the completion of the operation", + "example": "https://hyperswitch.io", "nullable": true }, - "mandate_data": { + "authentication_type": { "allOf": [ { - "$ref": "#/components/schemas/MandateData" + "$ref": "#/components/schemas/AuthenticationType" } ], + "default": "three_ds", "nullable": true }, - "mandate_id": { + "statement_descriptor_name": { "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", - "example": "mandate_iwer89rnjef349dni3", + "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", + "example": "Hyperswitch Router", "nullable": true, "maxLength": 255 }, - "browser_info": { - "type": "object", - "description": "Additional details required by 3DS 2.0", + "statement_descriptor_suffix": { + "type": "string", + "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 255 characters for the concatenated descriptor.", + "example": "Payment for shoes purchase", + "nullable": true, + "maxLength": 255 + }, + "next_action": { + "allOf": [ + { + "$ref": "#/components/schemas/NextActionData" + } + ], + "nullable": true + }, + "cancellation_reason": { + "type": "string", + "description": "If the payment was cancelled the reason provided here", + "nullable": true + }, + "error_code": { + "type": "string", + "description": "If there was an error while calling the connectors the code is received here", + "example": "E0001", + "nullable": true + }, + "error_message": { + "type": "string", + "description": "If there was an error while calling the connector the error message is received here", + "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": { @@ -9634,6 +13472,12 @@ ], "nullable": true }, + "connector_label": { + "type": "string", + "description": "The connector used for this payment along with the country and business details", + "example": "stripe_US_food", + "nullable": true + }, "business_country": { "allOf": [ { @@ -9644,16 +13488,12 @@ }, "business_label": { "type": "string", - "description": "Business label of the merchant for this payment", - "example": "food", + "description": "The business label of merchant for this payment", "nullable": true }, - "merchant_connector_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" - } - ], + "business_sub_label": { + "type": "string", + "description": "The business_sub_label for this payment", "nullable": true }, "allowed_payment_method_types": { @@ -9664,15 +13504,29 @@ "description": "Allowed Payment Method Types for a given PaymentIntent", "nullable": true }, - "business_sub_label": { + "ephemeral_key": { + "allOf": [ + { + "$ref": "#/components/schemas/EphemeralKeyCreateResponse" + } + ], + "nullable": true + }, + "manual_retry_allowed": { + "type": "boolean", + "description": "If true the payment can be retried with same or different payment method which means the confirm call can be made again.", + "nullable": true + }, + "connector_transaction_id": { "type": "string", - "description": "Business sub label for the payment", + "description": "A unique identifier for a payment provided by the connector", + "example": "993672945374576J", "nullable": true }, - "retry_action": { + "frm_message": { "allOf": [ { - "$ref": "#/components/schemas/RetryAction" + "$ref": "#/components/schemas/FrmMessage" } ], "nullable": true @@ -9698,17 +13552,23 @@ ], "nullable": true }, - "payment_link_object": { + "reference_id": { + "type": "string", + "description": "reference to the payment at connector side", + "example": "993672945374576J", + "nullable": true + }, + "payment_link": { "allOf": [ { - "$ref": "#/components/schemas/PaymentLinkObject" + "$ref": "#/components/schemas/PaymentLinkResponse" } ], "nullable": true }, "profile_id": { "type": "string", - "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", + "description": "The business profile that is associated with this payment", "nullable": true }, "surcharge_details": { @@ -9719,172 +13579,247 @@ ], "nullable": true }, - "payment_type": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentType" - } - ], + "attempt_count": { + "type": "integer", + "format": "int32", + "description": "total number of attempts associated with this payment" + }, + "merchant_decision": { + "type": "string", + "description": "Denotes the action(approve or reject) taken by merchant in case of manual review. Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment", + "nullable": true + }, + "merchant_connector_id": { + "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 + }, + "authorization_count": { + "type": "integer", + "format": "int32", + "description": "Total number of authorizations happened in an incremental_authorization payment", + "nullable": true + }, + "incremental_authorizations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncrementalAuthorizationResponse" + }, + "description": "List of incremental authorizations happened to the payment", + "nullable": true + }, + "expires_on": { + "type": "string", + "format": "date-time", + "description": "Date Time expiry of the payment", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, + "fingerprint": { + "type": "string", + "description": "Payment Fingerprint", "nullable": true } } }, - "PaymentsResponse": { + "PaymentsRetrieveRequest": { "type": "object", "required": [ - "status", - "amount", - "currency", - "payment_method", - "attempt_count" + "resource_id", + "force_sync" ], "properties": { - "payment_id": { - "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", - "example": "pay_mbabizu24mvu3mela5njyhpit4", - "nullable": true, - "maxLength": 30, - "minLength": 30 + "resource_id": { + "$ref": "#/components/schemas/PaymentIdType" }, "merchant_id": { "type": "string", - "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", - "example": "merchant_1668273825", - "nullable": true, - "maxLength": 255 - }, - "status": { - "allOf": [ - { - "$ref": "#/components/schemas/IntentStatus" - } - ], - "default": "requires_confirmation" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 100 + "description": "The identifier for the Merchant Account.", + "nullable": true }, - "amount_capturable": { - "type": "integer", - "format": "int64", - "description": "The maximum amount that could be captured from the payment", - "example": 6540, - "nullable": true, - "minimum": 100 + "force_sync": { + "type": "boolean", + "description": "Decider to enable or disable the connector call for retrieve request" }, - "amount_received": { - "type": "integer", - "format": "int64", - "description": "The amount which is already captured from the payment", - "example": 6540, - "nullable": true, - "minimum": 100 + "param": { + "type": "string", + "description": "The parameters passed to a retrieve request", + "nullable": true }, "connector": { "type": "string", - "description": "The connector used for the payment", - "example": "stripe", + "description": "The name of the connector", "nullable": true }, - "client_secret": { - "type": "string", - "description": "It's a token used for client side verification.", - "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], "nullable": true }, - "created": { + "client_secret": { "type": "string", - "format": "date-time", - "description": "Time when the payment was created", - "example": "2022-09-10T10:11:12Z", + "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK", "nullable": true }, - "currency": { - "$ref": "#/components/schemas/Currency" + "expand_captures": { + "type": "boolean", + "description": "If enabled provides list of captures linked to latest attempt", + "nullable": true }, - "customer_id": { + "expand_attempts": { + "type": "boolean", + "description": "If enabled provides list of attempts linked to payment intent", + "nullable": true + } + } + }, + "PaymentsSessionRequest": { + "type": "object", + "required": [ + "payment_id", + "client_secret", + "wallets" + ], + "properties": { + "payment_id": { "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, - "maxLength": 255 + "description": "The identifier for the payment" }, - "description": { + "client_secret": { "type": "string", - "description": "A description of the payment", - "example": "It's my first payment request", - "nullable": true + "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK" }, - "refunds": { + "wallets": { "type": "array", "items": { - "$ref": "#/components/schemas/RefundResponse" + "$ref": "#/components/schemas/PaymentMethodType" }, - "description": "List of refund that happened on this intent", - "nullable": true + "description": "The list of the supported wallets" }, - "disputes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DisputeResponsePaymentsRetrieve" - }, - "description": "List of dispute that happened on this intent", + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], "nullable": true + } + } + }, + "PaymentsSessionResponse": { + "type": "object", + "required": [ + "payment_id", + "client_secret", + "session_token" + ], + "properties": { + "payment_id": { + "type": "string", + "description": "The identifier for the payment" }, - "attempts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentAttemptResponse" - }, - "description": "List of attempts that happened on this intent", - "nullable": true + "client_secret": { + "type": "string", + "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK" }, - "captures": { + "session_token": { "type": "array", "items": { - "$ref": "#/components/schemas/CaptureResponse" + "$ref": "#/components/schemas/SessionToken" }, - "description": "List of captures done on latest attempt", - "nullable": true + "description": "The list of session token object" + } + } + }, + "PaymentsStartRequest": { + "type": "object", + "required": [ + "payment_id", + "merchant_id", + "attempt_id" + ], + "properties": { + "payment_id": { + "type": "string", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response." }, - "mandate_id": { + "merchant_id": { "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", - "example": "mandate_iwer89rnjef349dni3", + "description": "The identifier for the Merchant Account." + }, + "attempt_id": { + "type": "string", + "description": "The identifier for the payment transaction" + } + } + }, + "PaymentsUpdateRequest": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and ¥100 since ¥ is a zero-decimal currency", + "example": 6540, "nullable": true, - "maxLength": 255 + "minimum": 0 }, - "mandate_data": { + "currency": { "allOf": [ { - "$ref": "#/components/schemas/MandateData" + "$ref": "#/components/schemas/Currency" } ], "nullable": true }, - "setup_future_usage": { + "amount_to_capture": { + "type": "integer", + "format": "int64", + "description": "The Amount to be captured / debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, the default amount_to_capture will be the payment amount.", + "example": 6540, + "nullable": true + }, + "payment_id": { + "type": "string", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "pay_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 + }, + "merchant_id": { + "type": "string", + "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", + "example": "merchant_1668273825", + "nullable": true, + "maxLength": 255 + }, + "routing": { "allOf": [ { - "$ref": "#/components/schemas/FutureUsage" + "$ref": "#/components/schemas/StraightThroughAlgorithm" } ], "nullable": true }, - "off_session": { - "type": "boolean", - "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with confirm=true.", - "example": true, - "nullable": true - }, - "capture_on": { - "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", - "example": "2022-09-10T10:11:12Z", + "connector": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connector" + }, + "description": "This allows to manually select a connector with which the payment can go through", + "example": [ + "stripe", + "adyen" + ], "nullable": true }, "capture_method": { @@ -9895,24 +13830,16 @@ ], "nullable": true }, - "payment_method": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "payment_method_data": { + "authentication_type": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethod" + "$ref": "#/components/schemas/AuthenticationType" } ], + "default": "three_ds", "nullable": true }, - "payment_token": { - "type": "string", - "description": "Provide a reference to a stored payment method", - "example": "187282ab-40ef-47a9-9206-5099ba31e432", - "nullable": true - }, - "shipping": { + "billing": { "allOf": [ { "$ref": "#/components/schemas/Address" @@ -9920,169 +13847,200 @@ ], "nullable": true }, - "billing": { + "capture_on": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, + "confirm": { + "type": "boolean", + "description": "Whether to confirm the payment (if applicable)", + "default": false, + "example": true, + "nullable": true + }, + "customer": { "allOf": [ { - "$ref": "#/components/schemas/Address" + "$ref": "#/components/schemas/CustomerDetails" } ], "nullable": true }, - "order_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderDetailsWithAmount" - }, - "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", - "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]", - "nullable": true + "customer_id": { + "type": "string", + "description": "The identifier for the customer object. This field will be deprecated soon, use the customer object instead", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "nullable": true, + "maxLength": 255 }, "email": { "type": "string", - "description": "description: The customer's email address", + "description": "The customer's email address This field will be deprecated soon, use the customer object instead", "example": "johntest@test.com", "nullable": true, "maxLength": 255 }, "name": { "type": "string", - "description": "description: The customer's name", + "description": "The customer's name.\nThis field will be deprecated soon, use the customer object instead.", "example": "John Test", "nullable": true, "maxLength": 255 }, "phone": { "type": "string", - "description": "The customer's phone number", + "description": "The customer's phone number\nThis field will be deprecated soon, use the customer object instead", "example": "3141592653", "nullable": true, "maxLength": 255 }, - "return_url": { - "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://hyperswitch.io", - "nullable": true - }, - "authentication_type": { - "allOf": [ - { - "$ref": "#/components/schemas/AuthenticationType" - } - ], - "default": "three_ds", - "nullable": true - }, - "statement_descriptor_name": { - "type": "string", - "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", - "example": "Hyperswitch Router", - "nullable": true, - "maxLength": 255 - }, - "statement_descriptor_suffix": { + "phone_country_code": { "type": "string", - "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 255 characters for the concatenated descriptor.", - "example": "Payment for shoes purchase", + "description": "The country code for the customer phone number\nThis field will be deprecated soon, use the customer object instead", + "example": "+1", "nullable": true, "maxLength": 255 }, - "next_action": { - "allOf": [ - { - "$ref": "#/components/schemas/NextActionData" - } - ], + "off_session": { + "type": "boolean", + "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. When making a recurring payment by passing a mandate_id, this parameter is mandatory", + "example": true, "nullable": true }, - "cancellation_reason": { + "description": { "type": "string", - "description": "If the payment was cancelled the reason provided here", + "description": "A description for the payment", + "example": "It's my first payment request", "nullable": true }, - "error_code": { + "return_url": { "type": "string", - "description": "If there was an error while calling the connectors the code is received here", - "example": "E0001", + "description": "The URL to redirect after the completion of the operation", + "example": "https://hyperswitch.io", "nullable": true }, - "error_message": { - "type": "string", - "description": "If there was an error while calling the connector the error message is received here", - "example": "Failed while verifying the card", + "setup_future_usage": { + "allOf": [ + { + "$ref": "#/components/schemas/FutureUsage" + } + ], "nullable": true }, - "payment_experience": { + "payment_method_data": { "allOf": [ { - "$ref": "#/components/schemas/PaymentExperience" + "$ref": "#/components/schemas/PaymentMethodData" } ], "nullable": true }, - "payment_method_type": { + "payment_method": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/PaymentMethod" } ], "nullable": true }, - "connector_label": { + "payment_token": { "type": "string", - "description": "The connector used for this payment along with the country and business details", - "example": "stripe_US_food", + "description": "Provide a reference to a stored payment method", + "example": "187282ab-40ef-47a9-9206-5099ba31e432", "nullable": true }, - "business_country": { + "card_cvc": { + "type": "string", + "description": "This is used along with the payment_token field while collecting during saved card payments. This field will be deprecated soon, use the payment_method_data.card_token object instead", + "deprecated": true, + "nullable": true + }, + "shipping": { "allOf": [ { - "$ref": "#/components/schemas/CountryAlpha2" + "$ref": "#/components/schemas/Address" } ], "nullable": true }, - "business_label": { + "statement_descriptor_name": { "type": "string", - "description": "The business label of merchant for this payment", - "nullable": true + "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", + "example": "Hyperswitch Router", + "nullable": true, + "maxLength": 255 }, - "business_sub_label": { + "statement_descriptor_suffix": { "type": "string", - "description": "The business_sub_label for this payment", - "nullable": true + "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.", + "example": "Payment for shoes purchase", + "nullable": true, + "maxLength": 255 }, - "allowed_payment_method_types": { + "order_details": { "type": "array", "items": { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/OrderDetailsWithAmount" }, - "description": "Allowed Payment Method Types for a given PaymentIntent", + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, - "ephemeral_key": { + "mandate_data": { "allOf": [ { - "$ref": "#/components/schemas/EphemeralKeyCreateResponse" + "$ref": "#/components/schemas/MandateData" } ], "nullable": true }, - "manual_retry_allowed": { - "type": "boolean", - "description": "If true the payment can be retried with same or different payment method which means the confirm call can be made again.", + "browser_info": { + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" + } + ], "nullable": true }, - "connector_transaction_id": { - "type": "string", - "description": "A unique identifier for a payment provided by the connector", - "example": "993672945374576J", + "payment_experience": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentExperience" + } + ], + "nullable": true + }, + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } + ], + "nullable": true + }, + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], "nullable": true }, - "frm_message": { + "allowed_payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", + "nullable": true + }, + "retry_action": { "allOf": [ { - "$ref": "#/components/schemas/FrmMessage" + "$ref": "#/components/schemas/RetryAction" } ], "nullable": true @@ -10108,25 +14066,21 @@ ], "nullable": true }, - "reference_id": { - "type": "string", - "description": "reference to the payment at connector side", - "example": "993672945374576J", + "payment_link": { + "type": "boolean", + "description": "Whether to get the payment link (if applicable)", + "default": false, + "example": true, "nullable": true }, - "payment_link": { + "payment_link_config": { "allOf": [ { - "$ref": "#/components/schemas/PaymentLinkResponse" + "$ref": "#/components/schemas/PaymentCreatePaymentLinkConfig" } ], "nullable": true }, - "profile_id": { - "type": "string", - "description": "The business profile that is associated with this payment", - "nullable": true - }, "surcharge_details": { "allOf": [ { @@ -10135,185 +14089,234 @@ ], "nullable": true }, - "attempt_count": { + "payment_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentType" + } + ], + "nullable": true + }, + "request_incremental_authorization": { + "type": "boolean", + "description": "Request for an incremental authorization", + "nullable": true + }, + "session_expiry": { "type": "integer", "format": "int32", - "description": "total number of attempts associated with this payment" + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 }, - "merchant_decision": { - "type": "string", - "description": "Denotes the action(approve or reject) taken by merchant in case of manual review. Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment", + "frm_metadata": { + "description": "additional data related to some frm connectors", "nullable": true - }, - "merchant_connector_id": { + } + } + }, + "PayoutActionRequest": { + "type": "object", + "required": [ + "payout_id" + ], + "properties": { + "payout_id": { "type": "string", - "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", - "nullable": true + "description": "Unique identifier for the payout. This ensures idempotency for multiple payouts\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "payout_mbabizu24mvu3mela5njyhpit4", + "maxLength": 30, + "minLength": 30 } } }, - "PaymentsRetrieveRequest": { + "PayoutConnectors": { + "type": "string", + "enum": [ + "adyen", + "wise" + ] + }, + "PayoutCreateRequest": { "type": "object", "required": [ - "resource_id", - "force_sync" + "merchant_id", + "currency", + "confirm", + "payout_type", + "customer_id", + "auto_fulfill", + "client_secret", + "return_url", + "business_country", + "description", + "entity_type" ], "properties": { - "resource_id": { - "$ref": "#/components/schemas/PaymentIdType" + "payout_id": { + "type": "string", + "description": "Unique identifier for the payout. This ensures idempotency for multiple payouts\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "payout_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 }, "merchant_id": { "type": "string", - "description": "The identifier for the Merchant Account.", - "nullable": true + "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", + "example": "merchant_1668273825", + "maxLength": 255 }, - "force_sync": { - "type": "boolean", - "description": "Decider to enable or disable the connector call for retrieve request" + "amount": { + "type": "integer", + "format": "int64", + "description": "The payout amount. Amount for the payout in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", + "example": 1000 }, - "param": { - "type": "string", - "description": "The parameters passed to a retrieve request", + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "routing": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + ], "nullable": true }, "connector": { - "type": "string", - "description": "The name of the connector", + "type": "array", + "items": { + "$ref": "#/components/schemas/Connector" + }, + "description": "This allows the merchant to manually select a connector with which the payout can go through", + "example": [ + "wise", + "adyen" + ], "nullable": true }, - "merchant_connector_details": { + "confirm": { + "type": "boolean", + "description": "The boolean value to create payout with connector", + "default": false, + "example": true + }, + "payout_type": { + "$ref": "#/components/schemas/PayoutType" + }, + "payout_method_data": { "allOf": [ { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + "$ref": "#/components/schemas/PayoutMethodData" } ], "nullable": true }, - "client_secret": { - "type": "string", - "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK", + "billing": { + "type": "object", + "description": "The billing address for the payout", "nullable": true }, - "expand_captures": { - "type": "boolean", - "description": "If enabled provides list of captures linked to latest attempt", - "nullable": true + "customer_id": { + "type": "string", + "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - "expand_attempts": { + "auto_fulfill": { "type": "boolean", - "description": "If enabled provides list of attempts linked to payment intent", - "nullable": true - } - } - }, - "PaymentsSessionRequest": { - "type": "object", - "required": [ - "payment_id", - "client_secret", - "wallets" - ], - "properties": { - "payment_id": { + "description": "Set to true to confirm the payout without review, no further action required", + "default": false, + "example": true + }, + "email": { "type": "string", - "description": "The identifier for the payment" + "description": "description: The customer's email address", + "example": "johntest@test.com", + "nullable": true, + "maxLength": 255 }, - "client_secret": { + "name": { "type": "string", - "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK" + "description": "description: The customer's name", + "example": "John Test", + "nullable": true, + "maxLength": 255 }, - "wallets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "description": "The list of the supported wallets" + "phone": { + "type": "string", + "description": "The customer's phone number", + "example": "3141592653", + "nullable": true, + "maxLength": 255 }, - "merchant_connector_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" - } - ], - "nullable": true - } - } - }, - "PaymentsSessionResponse": { - "type": "object", - "required": [ - "payment_id", - "client_secret", - "session_token" - ], - "properties": { - "payment_id": { + "phone_country_code": { "type": "string", - "description": "The identifier for the payment" + "description": "The country code for the customer phone number", + "example": "+1", + "nullable": true, + "maxLength": 255 }, "client_secret": { "type": "string", - "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK" + "description": "It's a token used for client side verification.", + "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo" }, - "session_token": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionToken" - }, - "description": "The list of session token object" - } - } - }, - "PaymentsStartRequest": { - "type": "object", - "required": [ - "payment_id", - "merchant_id", - "attempt_id" - ], - "properties": { - "payment_id": { + "return_url": { + "type": "string", + "description": "The URL to redirect after the completion of the operation", + "example": "https://hyperswitch.io" + }, + "business_country": { + "$ref": "#/components/schemas/CountryAlpha2" + }, + "business_label": { "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response." + "description": "Business label of the merchant for this payout", + "example": "food", + "nullable": true }, - "merchant_id": { + "description": { "type": "string", - "description": "The identifier for the Merchant Account." + "description": "A description of the payout", + "example": "It's my first payout request" }, - "attempt_id": { + "entity_type": { + "$ref": "#/components/schemas/PayoutEntityType" + }, + "recurring": { + "type": "boolean", + "description": "Specifies whether or not the payout request is recurring", + "default": false, + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "payout_token": { "type": "string", - "description": "The identifier for the payment transaction" - } - } - }, - "PayoutActionRequest": { - "type": "object", - "required": [ - "payout_id" - ], - "properties": { - "payout_id": { + "description": "Provide a reference to a stored payment method", + "example": "187282ab-40ef-47a9-9206-5099ba31e432", + "nullable": true + }, + "profile_id": { "type": "string", - "description": "Unique identifier for the payout. This ensures idempotency for multiple payouts\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", - "example": "payout_mbabizu24mvu3mela5njyhpit4", - "maxLength": 30, - "minLength": 30 + "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", + "nullable": true } } }, - "PayoutConnectors": { - "type": "string", - "enum": [ - "adyen", - "wise" - ] - }, - "PayoutCreateRequest": { + "PayoutCreateResponse": { "type": "object", "required": [ + "payout_id", "merchant_id", + "amount", "currency", - "confirm", "payout_type", "customer_id", "auto_fulfill", @@ -10321,14 +14324,17 @@ "return_url", "business_country", "description", - "entity_type" + "entity_type", + "status", + "error_message", + "error_code", + "profile_id" ], "properties": { "payout_id": { "type": "string", "description": "Unique identifier for the payout. This ensures idempotency for multiple payouts\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", "example": "payout_mbabizu24mvu3mela5njyhpit4", - "nullable": true, "maxLength": 30, "minLength": 30 }, @@ -10347,43 +14353,15 @@ "currency": { "$ref": "#/components/schemas/Currency" }, - "routing": { - "allOf": [ - { - "$ref": "#/components/schemas/RoutingAlgorithm" - } - ], - "nullable": true - }, "connector": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Connector" - }, - "description": "This allows the merchant to manually select a connector with which the payout can go through", - "example": [ - "wise", - "adyen" - ], + "type": "string", + "description": "The connector used for the payout", + "example": "wise", "nullable": true }, - "confirm": { - "type": "boolean", - "description": "The boolean value to create payout with connector", - "default": false, - "example": true - }, "payout_type": { "$ref": "#/components/schemas/PayoutType" }, - "payout_method_data": { - "allOf": [ - { - "$ref": "#/components/schemas/PayoutMethodData" - } - ], - "nullable": true - }, "billing": { "type": "object", "description": "The billing address for the payout", @@ -10467,877 +14445,1149 @@ "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", "nullable": true }, - "payout_token": { + "status": { + "$ref": "#/components/schemas/PayoutStatus" + }, + "error_message": { + "type": "string", + "description": "If there was an error while calling the connector the error message is received here", + "example": "Failed while verifying the card" + }, + "error_code": { + "type": "string", + "description": "If there was an error while calling the connectors the code is received here", + "example": "E0001" + }, + "profile_id": { + "type": "string", + "description": "The business profile that is associated with this payment" + } + } + }, + "PayoutEntityType": { + "type": "string", + "enum": [ + "Individual", + "Company", + "NonProfit", + "PublicSector", + "lowercase", + "Personal" + ] + }, + "PayoutMethodData": { + "oneOf": [ + { + "type": "object", + "required": [ + "card" + ], + "properties": { + "card": { + "$ref": "#/components/schemas/Card" + } + } + }, + { + "type": "object", + "required": [ + "bank" + ], + "properties": { + "bank": { + "$ref": "#/components/schemas/Bank" + } + } + } + ] + }, + "PayoutRequest": { + "oneOf": [ + { + "type": "object", + "required": [ + "PayoutActionRequest" + ], + "properties": { + "PayoutActionRequest": { + "$ref": "#/components/schemas/PayoutActionRequest" + } + } + }, + { + "type": "object", + "required": [ + "PayoutCreateRequest" + ], + "properties": { + "PayoutCreateRequest": { + "$ref": "#/components/schemas/PayoutCreateRequest" + } + } + }, + { + "type": "object", + "required": [ + "PayoutRetrieveRequest" + ], + "properties": { + "PayoutRetrieveRequest": { + "$ref": "#/components/schemas/PayoutRetrieveRequest" + } + } + } + ] + }, + "PayoutRetrieveBody": { + "type": "object", + "properties": { + "force_sync": { + "type": "boolean", + "nullable": true + } + } + }, + "PayoutRetrieveRequest": { + "type": "object", + "required": [ + "payout_id" + ], + "properties": { + "payout_id": { + "type": "string", + "description": "Unique identifier for the payout. This ensures idempotency for multiple payouts\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "payout_mbabizu24mvu3mela5njyhpit4", + "maxLength": 30, + "minLength": 30 + }, + "force_sync": { + "type": "boolean", + "description": "`force_sync` with the connector to get payout details\n(defaults to false)", + "default": false, + "example": true, + "nullable": true + } + } + }, + "PayoutStatus": { + "type": "string", + "enum": [ + "success", + "failed", + "cancelled", + "pending", + "ineligible", + "requires_creation", + "requires_payout_method_data", + "requires_fulfillment" + ] + }, + "PayoutType": { + "type": "string", + "enum": [ + "card", + "bank" + ] + }, + "PaypalRedirection": { + "type": "object" + }, + "PaypalSessionTokenResponse": { + "type": "object", + "required": [ + "session_token" + ], + "properties": { + "session_token": { "type": "string", - "description": "Provide a reference to a stored payment method", - "example": "187282ab-40ef-47a9-9206-5099ba31e432", + "description": "The session token for PayPal" + } + } + }, + "PhoneDetails": { + "type": "object", + "properties": { + "number": { + "type": "string", + "description": "The contact number", + "example": "9999999999", "nullable": true }, - "profile_id": { + "country_code": { "type": "string", - "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", + "description": "The country code attached to the number", + "example": "+1", "nullable": true } } }, - "PayoutCreateResponse": { + "PrimaryBusinessDetails": { "type": "object", "required": [ - "payout_id", - "merchant_id", - "amount", - "currency", - "payout_type", - "customer_id", - "auto_fulfill", - "client_secret", - "return_url", - "business_country", - "description", - "entity_type", - "status", - "error_message", - "error_code" + "country", + "business" ], "properties": { - "payout_id": { - "type": "string", - "description": "Unique identifier for the payout. This ensures idempotency for multiple payouts\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", - "example": "payout_mbabizu24mvu3mela5njyhpit4", - "maxLength": 30, - "minLength": 30 + "country": { + "$ref": "#/components/schemas/CountryAlpha2" }, - "merchant_id": { + "business": { "type": "string", - "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", - "example": "merchant_1668273825", - "maxLength": 255 + "example": "food" + } + } + }, + "ProductType": { + "type": "string", + "enum": [ + "physical", + "digital", + "travel", + "ride", + "event", + "accommodation" + ] + }, + "ProfileDefaultRoutingConfig": { + "type": "object", + "required": [ + "profile_id", + "connectors" + ], + "properties": { + "profile_id": { + "type": "string" }, - "amount": { + "connectors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } + }, + "ProgramConnectorSelection": { + "type": "object", + "description": "The program, having a default connector selection and\na bunch of rules. Also can hold arbitrary metadata.", + "required": [ + "defaultSelection", + "rules", + "metadata" + ], + "properties": { + "defaultSelection": { + "$ref": "#/components/schemas/ConnectorSelection" + }, + "rules": { + "$ref": "#/components/schemas/RuleConnectorSelection" + }, + "metadata": { + "type": "object", + "additionalProperties": {} + } + } + }, + "ReceiverDetails": { + "type": "object", + "required": [ + "amount_received" + ], + "properties": { + "amount_received": { "type": "integer", "format": "int64", - "description": "The payout amount. Amount for the payout in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 1000 + "description": "The amount received by receiver" }, - "currency": { - "$ref": "#/components/schemas/Currency" + "amount_charged": { + "type": "integer", + "format": "int64", + "description": "The amount charged by ACH", + "nullable": true }, - "connector": { + "amount_remaining": { + "type": "integer", + "format": "int64", + "description": "The amount remaining to be sent via ACH", + "nullable": true + } + } + }, + "ReconStatus": { + "type": "string", + "enum": [ + "not_requested", + "requested", + "active", + "disabled" + ] + }, + "RedirectResponse": { + "type": "object", + "properties": { + "param": { "type": "string", - "description": "The connector used for the payout", - "example": "wise", "nullable": true }, - "payout_type": { - "$ref": "#/components/schemas/PayoutType" - }, - "billing": { + "json_payload": { "type": "object", - "description": "The billing address for the payout", + "nullable": true + } + } + }, + "RefundListRequest": { + "allOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/TimeRange" + } + ], "nullable": true }, - "customer_id": { - "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + { + "type": "object", + "properties": { + "payment_id": { + "type": "string", + "description": "The identifier for the payment", + "nullable": true + }, + "refund_id": { + "type": "string", + "description": "The identifier for the refund", + "nullable": true + }, + "profile_id": { + "type": "string", + "description": "The identifier for business profile", + "nullable": true + }, + "limit": { + "type": "integer", + "format": "int64", + "description": "Limit on the number of objects to return", + "nullable": true + }, + "offset": { + "type": "integer", + "format": "int64", + "description": "The starting point within a list of objects", + "nullable": true + }, + "connector": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of connectors to filter refunds list", + "nullable": true + }, + "currency": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + }, + "description": "The list of currencies to filter refunds list", + "nullable": true + }, + "refund_status": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RefundStatus" + }, + "description": "The list of refund statuses to filter refunds list", + "nullable": true + } + } + } + ] + }, + "RefundListResponse": { + "type": "object", + "required": [ + "count", + "total_count", + "data" + ], + "properties": { + "count": { + "type": "integer", + "description": "The number of refunds included in the list", + "minimum": 0 }, - "auto_fulfill": { - "type": "boolean", - "description": "Set to true to confirm the payout without review, no further action required", - "default": false, - "example": true + "total_count": { + "type": "integer", + "format": "int64", + "description": "The total number of refunds in the list" }, - "email": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RefundResponse" + }, + "description": "The List of refund response object" + } + } + }, + "RefundRequest": { + "type": "object", + "required": [ + "payment_id" + ], + "properties": { + "payment_id": { "type": "string", - "description": "description: The customer's email address", - "example": "johntest@test.com", + "description": "The payment id against which refund is to be intiated", + "example": "pay_mbabizu24mvu3mela5njyhpit4", + "maxLength": 30, + "minLength": 30 + }, + "refund_id": { + "type": "string", + "description": "Unique Identifier for the Refund. This is to ensure idempotency for multiple partial refunds initiated against the same payment. If this is not passed by the merchant, this field shall be auto generated and provided in the API response. It is recommended to generate uuid(v4) as the refund_id.", + "example": "ref_mbabizu24mvu3mela5njyhpit4", "nullable": true, - "maxLength": 255 + "maxLength": 30, + "minLength": 30 }, - "name": { + "merchant_id": { "type": "string", - "description": "description: The customer's name", - "example": "John Test", + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", "nullable": true, "maxLength": 255 }, - "phone": { - "type": "string", - "description": "The customer's phone number", - "example": "3141592653", + "amount": { + "type": "integer", + "format": "int64", + "description": "Total amount for which the refund is to be initiated. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, this will default to the full payment amount", + "example": 6540, "nullable": true, - "maxLength": 255 + "minimum": 100 }, - "phone_country_code": { + "reason": { "type": "string", - "description": "The country code for the customer phone number", - "example": "+1", + "description": "Reason for the refund. Often useful for displaying to users and your customer support executive. In case the payment went through Stripe, this field needs to be passed with one of these enums: `duplicate`, `fraudulent`, or `requested_by_customer`", + "example": "Customer returned the product", "nullable": true, "maxLength": 255 }, - "client_secret": { - "type": "string", - "description": "It's a token used for client side verification.", - "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo" + "refund_type": { + "allOf": [ + { + "$ref": "#/components/schemas/RefundType" + } + ], + "default": "Instant", + "nullable": true }, - "return_url": { - "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://hyperswitch.io" + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true }, - "business_country": { - "$ref": "#/components/schemas/CountryAlpha2" + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], + "nullable": true + } + } + }, + "RefundResponse": { + "type": "object", + "required": [ + "refund_id", + "payment_id", + "amount", + "currency", + "status", + "connector" + ], + "properties": { + "refund_id": { + "type": "string", + "description": "Unique Identifier for the refund" }, - "business_label": { + "payment_id": { "type": "string", - "description": "Business label of the merchant for this payout", - "example": "food", - "nullable": true + "description": "The payment id against which refund is intiated" }, - "description": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The refund amount, which should be less than or equal to the total payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc" + }, + "currency": { "type": "string", - "description": "A description of the payout", - "example": "It's my first payout request" + "description": "The three-letter ISO currency code" }, - "entity_type": { - "$ref": "#/components/schemas/PayoutEntityType" + "status": { + "$ref": "#/components/schemas/RefundStatus" }, - "recurring": { - "type": "boolean", - "description": "Specifies whether or not the payout request is recurring", - "default": false, + "reason": { + "type": "string", + "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", "nullable": true }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object", "nullable": true }, - "status": { - "$ref": "#/components/schemas/PayoutStatus" - }, "error_message": { "type": "string", - "description": "If there was an error while calling the connector the error message is received here", - "example": "Failed while verifying the card" + "description": "The error message", + "nullable": true }, "error_code": { "type": "string", - "description": "If there was an error while calling the connectors the code is received here", - "example": "E0001" + "description": "The code for the error", + "nullable": true }, - "profile_id": { + "created_at": { "type": "string", - "description": "The business profile that is associated with this payment", + "format": "date-time", + "description": "The timestamp at which refund is created", "nullable": true - } - } - }, - "PayoutEntityType": { - "type": "string", - "enum": [ - "Individual", - "Company", - "NonProfit", - "PublicSector", - "lowercase", - "Personal" - ] - }, - "PayoutMethodData": { - "oneOf": [ - { - "type": "object", - "required": [ - "card" - ], - "properties": { - "card": { - "$ref": "#/components/schemas/Card" - } - } }, - { - "type": "object", - "required": [ - "bank" - ], - "properties": { - "bank": { - "$ref": "#/components/schemas/Bank" - } - } - } - ] - }, - "PayoutRequest": { - "oneOf": [ - { - "type": "object", - "required": [ - "PayoutActionRequest" - ], - "properties": { - "PayoutActionRequest": { - "$ref": "#/components/schemas/PayoutActionRequest" - } - } + "updated_at": { + "type": "string", + "format": "date-time", + "description": "The timestamp at which refund is updated", + "nullable": true }, - { - "type": "object", - "required": [ - "PayoutCreateRequest" - ], - "properties": { - "PayoutCreateRequest": { - "$ref": "#/components/schemas/PayoutCreateRequest" - } - } + "connector": { + "type": "string", + "description": "The connector used for the refund and the corresponding payment", + "example": "stripe" }, - { - "type": "object", - "required": [ - "PayoutRetrieveRequest" - ], - "properties": { - "PayoutRetrieveRequest": { - "$ref": "#/components/schemas/PayoutRetrieveRequest" - } - } - } - ] - }, - "PayoutRetrieveBody": { - "type": "object", - "properties": { - "force_sync": { - "type": "boolean", - "nullable": true - } - } - }, - "PayoutRetrieveRequest": { - "type": "object", - "required": [ - "payout_id" - ], - "properties": { - "payout_id": { + "profile_id": { "type": "string", - "description": "Unique identifier for the payout. This ensures idempotency for multiple payouts\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", - "example": "payout_mbabizu24mvu3mela5njyhpit4", - "maxLength": 30, - "minLength": 30 + "description": "The id of business profile for this refund", + "nullable": true }, - "force_sync": { - "type": "boolean", - "description": "`force_sync` with the connector to get payout details\n(defaults to false)", - "default": false, - "example": true, + "merchant_connector_id": { + "type": "string", + "description": "The merchant_connector_id of the processor through which this payment went through", "nullable": true } } }, - "PayoutStatus": { - "type": "string", - "enum": [ - "success", - "failed", - "cancelled", - "pending", - "ineligible", - "requires_creation", - "requires_payout_method_data", - "requires_fulfillment" - ] - }, - "PayoutType": { + "RefundStatus": { "type": "string", + "description": "The status for refunds", "enum": [ - "card", - "bank" - ] - }, - "PaypalRedirection": { - "type": "object" - }, - "PaypalSessionTokenResponse": { - "type": "object", - "required": [ - "session_token" - ], - "properties": { - "session_token": { - "type": "string", - "description": "The session token for PayPal" - } - } + "succeeded", + "failed", + "pending", + "review" + ] }, - "PhoneDetails": { + "RefundType": { + "type": "string", + "description": "To indicate whether to refund needs to be instant or scheduled", + "enum": [ + "scheduled", + "instant" + ] + }, + "RefundUpdateRequest": { "type": "object", "properties": { - "number": { + "reason": { "type": "string", - "description": "The contact number", - "example": "9999999999", - "nullable": true + "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", + "example": "Customer returned the product", + "nullable": true, + "maxLength": 255 }, - "country_code": { - "type": "string", - "description": "The country code attached to the number", - "example": "+1", + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", "nullable": true } } }, - "PrimaryBusinessDetails": { + "RequestPaymentMethodTypes": { "type": "object", "required": [ - "country", - "business" + "payment_method_type", + "recurring_enabled", + "installment_payment_enabled" ], "properties": { - "country": { - "$ref": "#/components/schemas/CountryAlpha2" + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethodType" }, - "business": { - "type": "string", - "example": "food" + "payment_experience": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentExperience" + } + ], + "nullable": true + }, + "card_networks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CardNetwork" + }, + "nullable": true + }, + "accepted_currencies": { + "allOf": [ + { + "$ref": "#/components/schemas/AcceptedCurrencies" + } + ], + "nullable": true + }, + "accepted_countries": { + "allOf": [ + { + "$ref": "#/components/schemas/AcceptedCountries" + } + ], + "nullable": true + }, + "minimum_amount": { + "type": "integer", + "format": "int32", + "description": "Minimum amount supported by the processor. To be represented in the lowest denomination of the target currency (For example, for USD it should be in cents)", + "example": 1, + "nullable": true + }, + "maximum_amount": { + "type": "integer", + "format": "int32", + "description": "Maximum amount supported by the processor. To be represented in the lowest denomination of\nthe target currency (For example, for USD it should be in cents)", + "example": 1313, + "nullable": true + }, + "recurring_enabled": { + "type": "boolean", + "description": "Boolean to enable recurring payments / mandates. Default is true.", + "default": true, + "example": false + }, + "installment_payment_enabled": { + "type": "boolean", + "description": "Boolean to enable installment / EMI / BNPL payments. Default is true.", + "default": true, + "example": false } } }, - "ReceiverDetails": { + "RequestSurchargeDetails": { "type": "object", "required": [ - "amount_received" + "surcharge_amount" ], "properties": { - "amount_received": { - "type": "integer", - "format": "int64", - "description": "The amount received by receiver" - }, - "amount_charged": { + "surcharge_amount": { "type": "integer", - "format": "int64", - "description": "The amount charged by ACH", - "nullable": true + "format": "int64" }, - "amount_remaining": { + "tax_amount": { "type": "integer", "format": "int64", - "description": "The amount remaining to be sent via ACH", "nullable": true } } }, - "ReconStatus": { - "type": "string", - "enum": [ - "not_requested", - "requested", - "active", - "disabled" - ] - }, - "RedirectResponse": { + "RequiredFieldInfo": { "type": "object", + "description": "Required fields info used while listing the payment_method_data", + "required": [ + "required_field", + "display_name", + "field_type" + ], "properties": { - "param": { + "required_field": { "type": "string", - "nullable": true + "description": "Required field for a payment_method through a payment_method_type" }, - "json_payload": { - "type": "object", + "display_name": { + "type": "string", + "description": "Display name of the required field in the front-end" + }, + "field_type": { + "$ref": "#/components/schemas/FieldType" + }, + "value": { + "type": "string", "nullable": true } } }, - "RefundListRequest": { - "allOf": [ - { - "allOf": [ - { - "$ref": "#/components/schemas/TimeRange" - } - ], - "nullable": true - }, - { - "type": "object", - "properties": { - "payment_id": { - "type": "string", - "description": "The identifier for the payment", - "nullable": true - }, - "refund_id": { - "type": "string", - "description": "The identifier for the refund", - "nullable": true - }, - "profile_id": { - "type": "string", - "description": "The identifier for business profile", - "nullable": true - }, - "limit": { - "type": "integer", - "format": "int64", - "description": "Limit on the number of objects to return", - "nullable": true - }, - "offset": { - "type": "integer", - "format": "int64", - "description": "The starting point within a list of objects", - "nullable": true - }, - "connector": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The list of connectors to filter refunds list", - "nullable": true - }, - "currency": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" - }, - "description": "The list of currencies to filter refunds list", - "nullable": true - }, - "refund_status": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefundStatus" - }, - "description": "The list of refund statuses to filter refunds list", - "nullable": true - } - } - } - ] - }, - "RefundListResponse": { + "RetrieveApiKeyResponse": { "type": "object", + "description": "The response body for retrieving an API Key.", "required": [ - "count", - "total_count", - "data" + "key_id", + "merchant_id", + "name", + "prefix", + "created", + "expiration" ], "properties": { - "count": { - "type": "integer", - "description": "The number of refunds included in the list", - "minimum": 0 + "key_id": { + "type": "string", + "description": "The identifier for the API Key.", + "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", + "maxLength": 64 }, - "total_count": { - "type": "integer", - "format": "int64", - "description": "The total number of refunds in the list" + "merchant_id": { + "type": "string", + "description": "The identifier for the Merchant Account.", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 64 + }, + "name": { + "type": "string", + "description": "The unique name for the API Key to help you identify it.", + "example": "Sandbox integration key", + "maxLength": 64 + }, + "description": { + "type": "string", + "description": "The description to provide more context about the API Key.", + "example": "Key used by our developers to integrate with the sandbox environment", + "nullable": true, + "maxLength": 256 + }, + "prefix": { + "type": "string", + "description": "The first few characters of the plaintext API Key to help you identify it.", + "maxLength": 64 + }, + "created": { + "type": "string", + "format": "date-time", + "description": "The time at which the API Key was created.", + "example": "2022-09-10T10:11:12Z" }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefundResponse" - }, - "description": "The List of refund response object" + "expiration": { + "$ref": "#/components/schemas/ApiKeyExpiration" } } }, - "RefundRequest": { + "RetrievePaymentLinkRequest": { + "type": "object", + "properties": { + "client_secret": { + "type": "string", + "nullable": true + } + } + }, + "RetrievePaymentLinkResponse": { "type": "object", "required": [ - "payment_id" + "payment_link_id", + "merchant_id", + "link_to_pay", + "amount", + "created_at", + "status" ], "properties": { - "refund_id": { - "type": "string", - "description": "Unique Identifier for the Refund. This is to ensure idempotency for multiple partial refund initiated against the same payment. If the identifiers is not defined by the merchant, this filed shall be auto generated and provide in the API response. It is recommended to generate uuid(v4) as the refund_id.", - "example": "ref_mbabizu24mvu3mela5njyhpit4", - "nullable": true, - "maxLength": 30, - "minLength": 30 - }, - "payment_id": { - "type": "string", - "description": "Total amount for which the refund is to be initiated. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc. If not provided, this will default to the full payment amount", - "example": "pay_mbabizu24mvu3mela5njyhpit4", - "maxLength": 30, - "minLength": 30 + "payment_link_id": { + "type": "string" }, "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, - "maxLength": 255 + "type": "string" + }, + "link_to_pay": { + "type": "string" }, "amount": { "type": "integer", - "format": "int64", - "description": "Total amount for which the refund is to be initiated. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, this will default to the full payment amount", - "example": 6540, - "nullable": true, - "minimum": 100 + "format": "int64" }, - "reason": { + "created_at": { "type": "string", - "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", - "example": "Customer returned the product", - "nullable": true, - "maxLength": 255 + "format": "date-time" }, - "refund_type": { - "allOf": [ - { - "$ref": "#/components/schemas/RefundType" - } - ], - "default": "Instant", + "expiry": { + "type": "string", + "format": "date-time", "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": { + "type": "string", "nullable": true }, - "merchant_connector_details": { + "status": { + "$ref": "#/components/schemas/PaymentLinkStatus" + }, + "currency": { "allOf": [ { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + "$ref": "#/components/schemas/Currency" } ], "nullable": true } } }, - "RefundResponse": { + "RetryAction": { + "type": "string", + "enum": [ + "manual_retry", + "requeue" + ] + }, + "RevokeApiKeyResponse": { "type": "object", + "description": "The response body for revoking an API Key.", "required": [ - "refund_id", - "payment_id", - "amount", - "currency", - "status", - "connector" + "merchant_id", + "key_id", + "revoked" ], "properties": { - "refund_id": { + "merchant_id": { "type": "string", - "description": "The identifier for refund" + "description": "The identifier for the Merchant Account.", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 64 }, - "payment_id": { + "key_id": { "type": "string", - "description": "The identifier for payment" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The refund amount, which should be less than or equal to the total payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc" + "description": "The identifier for the API Key.", + "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", + "maxLength": 64 }, - "currency": { + "revoked": { + "type": "boolean", + "description": "Indicates whether the API key was revoked or not.", + "example": "true" + } + } + }, + "RewardData": { + "type": "object", + "required": [ + "merchant_id" + ], + "properties": { + "merchant_id": { "type": "string", - "description": "The three-letter ISO currency code" + "description": "The merchant ID with which we have to call the connector" + } + } + }, + "RoutableChoiceKind": { + "type": "string", + "enum": [ + "OnlyConnector", + "FullStruct" + ] + }, + "RoutableConnectorChoice": { + "type": "object", + "description": "Routable Connector chosen for a payment", + "required": [ + "connector" + ], + "properties": { + "connector": { + "$ref": "#/components/schemas/RoutableConnectors" }, - "reason": { + "sub_label": { "type": "string", - "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", "nullable": true - }, - "status": { - "$ref": "#/components/schemas/RefundStatus" - }, - "metadata": { + } + } + }, + "RoutableConnectors": { + "type": "string", + "description": "Connectors eligible for payments routing", + "enum": [ + "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", + "opennode", + "payme", + "paypal", + "payu", + "placetopay", + "powertranz", + "prophetpay", + "rapyd", + "riskified", + "shift4", + "signifyd", + "square", + "stax", + "stripe", + "trustpay", + "tsys", + "volt", + "wise", + "worldline", + "worldpay", + "zen" + ] + }, + "RoutingAlgorithm": { + "oneOf": [ + { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object", - "nullable": true - }, - "error_message": { - "type": "string", - "description": "The error message", - "nullable": true - }, - "error_code": { - "type": "string", - "description": "The code for the error", - "nullable": true - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "The timestamp at which refund is created", - "nullable": true + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "single" + ] + }, + "data": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } }, - "updated_at": { - "type": "string", - "format": "date-time", - "description": "The timestamp at which refund is updated", - "nullable": true + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "priority" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } }, - "connector": { - "type": "string", - "description": "The connector used for the refund and the corresponding payment", - "example": "stripe" + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "volume_split" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectorVolumeSplit" + } + } + } }, - "profile_id": { - "type": "string", - "nullable": true + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "advanced" + ] + }, + "data": { + "$ref": "#/components/schemas/ProgramConnectorSelection" + } + } } + ], + "description": "Routing Algorithm kind", + "discriminator": { + "propertyName": "type" } }, - "RefundStatus": { - "type": "string", - "description": "The status for refunds", - "enum": [ - "succeeded", - "failed", - "pending", - "review" - ] - }, - "RefundType": { + "RoutingAlgorithmKind": { "type": "string", "enum": [ - "scheduled", - "instant" + "single", + "priority", + "volume_split", + "advanced" ] }, - "RefundUpdateRequest": { + "RoutingConfigRequest": { "type": "object", "properties": { - "reason": { + "name": { "type": "string", - "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", - "example": "Customer returned the product", - "nullable": true, - "maxLength": 255 - }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - } - } - }, - "RequestSurchargeDetails": { - "type": "object", - "required": [ - "surcharge_amount" - ], - "properties": { - "surcharge_amount": { - "type": "integer", - "format": "int64" - }, - "tax_amount": { - "type": "integer", - "format": "int64", "nullable": true - } - } - }, - "RequiredFieldInfo": { - "type": "object", - "description": "Required fields info used while listing the payment_method_data", - "required": [ - "required_field", - "display_name", - "field_type" - ], - "properties": { - "required_field": { - "type": "string", - "description": "Required field for a payment_method through a payment_method_type" }, - "display_name": { + "description": { "type": "string", - "description": "Display name of the required field in the front-end" + "nullable": true }, - "field_type": { - "$ref": "#/components/schemas/FieldType" + "algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + ], + "nullable": true }, - "value": { + "profile_id": { "type": "string", "nullable": true } } }, - "RetrieveApiKeyResponse": { + "RoutingDictionary": { "type": "object", - "description": "The response body for retrieving an API Key.", "required": [ - "key_id", "merchant_id", - "name", - "prefix", - "created", - "expiration" + "records" ], "properties": { - "key_id": { - "type": "string", - "description": "The identifier for the API Key.", - "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", - "maxLength": 64 - }, "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account.", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 64 - }, - "name": { - "type": "string", - "description": "The unique name for the API Key to help you identify it.", - "example": "Sandbox integration key", - "maxLength": 64 - }, - "description": { - "type": "string", - "description": "The description to provide more context about the API Key.", - "example": "Key used by our developers to integrate with the sandbox environment", - "nullable": true, - "maxLength": 256 - }, - "prefix": { - "type": "string", - "description": "The first few characters of the plaintext API Key to help you identify it.", - "maxLength": 64 - }, - "created": { - "type": "string", - "format": "date-time", - "description": "The time at which the API Key was created.", - "example": "2022-09-10T10:11:12Z" + "type": "string" }, - "expiration": { - "$ref": "#/components/schemas/ApiKeyExpiration" - } - } - }, - "RetrievePaymentLinkRequest": { - "type": "object", - "properties": { - "client_secret": { + "active_id": { "type": "string", "nullable": true + }, + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } } } }, - "RetrievePaymentLinkResponse": { + "RoutingDictionaryRecord": { "type": "object", "required": [ - "payment_link_id", - "payment_id", - "merchant_id", - "link_to_pay", - "amount", + "id", + "name", + "kind", + "description", "created_at", - "last_modified_at" + "modified_at" ], "properties": { - "payment_link_id": { + "id": { "type": "string" }, - "payment_id": { + "name": { "type": "string" }, - "merchant_id": { - "type": "string" + "kind": { + "$ref": "#/components/schemas/RoutingAlgorithmKind" }, - "link_to_pay": { + "description": { "type": "string" }, - "amount": { + "created_at": { "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 + "modified_at": { + "type": "integer", + "format": "int64" } } }, - "RetryAction": { - "type": "string", - "enum": [ - "manual_retry", - "requeue" + "RoutingKind": { + "oneOf": [ + { + "$ref": "#/components/schemas/RoutingDictionary" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } + } ] }, - "RevokeApiKeyResponse": { + "RoutingRetrieveResponse": { "type": "object", - "description": "The response body for revoking an API Key.", - "required": [ - "merchant_id", - "key_id", - "revoked" - ], + "description": "Response of the retrieved routing configs for a merchant account", "properties": { - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account.", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 64 - }, - "key_id": { - "type": "string", - "description": "The identifier for the API Key.", - "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", - "maxLength": 64 - }, - "revoked": { - "type": "boolean", - "description": "Indicates whether the API key was revoked or not.", - "example": "true" + "algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantRoutingAlgorithm" + } + ], + "nullable": true } } }, - "RewardData": { + "RuleConnectorSelection": { "type": "object", + "description": "Represents a rule\n\n```text\nrule_name: [stripe, adyen, checkout]\n{\npayment.method = card {\npayment.method.cardtype = (credit, debit) {\npayment.method.network = (amex, rupay, diners)\n}\n\npayment.method.cardtype = credit\n}\n}\n```", "required": [ - "merchant_id" + "name", + "connectorSelection", + "statements" ], "properties": { - "merchant_id": { - "type": "string", - "description": "The merchant ID with which we have to call the connector" + "name": { + "type": "string" + }, + "connectorSelection": { + "$ref": "#/components/schemas/ConnectorSelection" + }, + "statements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IfStatement" + } } } }, - "RoutingAlgorithm": { - "type": "string", - "description": "The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom'", - "enum": [ - "round_robin", - "max_conversion", - "min_cost", - "custom" - ], - "example": "custom" - }, "SamsungPayWalletData": { "type": "object", "required": [ @@ -11398,9 +15648,6 @@ "SepaBankTransfer": { "type": "object", "required": [ - "bank_name", - "bank_country_code", - "bank_city", "iban", "bic" ], @@ -11408,15 +15655,22 @@ "bank_name": { "type": "string", "description": "Bank name", - "example": "Deutsche Bank" + "example": "Deutsche Bank", + "nullable": true }, "bank_country_code": { - "$ref": "#/components/schemas/CountryAlpha2" + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true }, "bank_city": { "type": "string", "description": "Bank city", - "example": "California" + "example": "California", + "nullable": true }, "iban": { "type": "string", @@ -11592,6 +15846,176 @@ } } }, + "StraightThroughAlgorithm": { + "oneOf": [ + { + "type": "object", + "title": "Single", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "single" + ] + }, + "data": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + }, + { + "type": "object", + "title": "Priority", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "priority" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } + }, + { + "type": "object", + "title": "VolumeSplit", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "volume_split" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectorVolumeSplit" + } + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "SurchargeDetailsResponse": { + "type": "object", + "required": [ + "surcharge", + "display_surcharge_amount", + "display_tax_on_surcharge_amount", + "display_total_surcharge_amount", + "display_final_amount" + ], + "properties": { + "surcharge": { + "$ref": "#/components/schemas/SurchargeResponse" + }, + "tax_on_surcharge": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargePercentage" + } + ], + "nullable": true + }, + "display_surcharge_amount": { + "type": "number", + "format": "double", + "description": "surcharge amount for this payment" + }, + "display_tax_on_surcharge_amount": { + "type": "number", + "format": "double", + "description": "tax on surcharge amount for this payment" + }, + "display_total_surcharge_amount": { + "type": "number", + "format": "double", + "description": "sum of display_surcharge_amount and display_tax_on_surcharge_amount" + }, + "display_final_amount": { + "type": "number", + "format": "double", + "description": "sum of original amount," + } + } + }, + "SurchargePercentage": { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "number", + "format": "float" + } + } + }, + "SurchargeResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fixed" + ] + }, + "value": { + "type": "integer", + "format": "int64", + "description": "Fixed Surcharge value" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "rate" + ] + }, + "value": { + "$ref": "#/components/schemas/SurchargePercentage" + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, "SwishQrData": { "type": "object" }, @@ -11666,6 +16090,157 @@ } } }, + "ValueType": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "number" + ] + }, + "value": { + "type": "integer", + "format": "int64", + "description": "Represents a number literal" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "enum_variant" + ] + }, + "value": { + "type": "string", + "description": "Represents an enum variant" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "metadata_variant" + ] + }, + "value": { + "$ref": "#/components/schemas/MetadataValue" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "str_value" + ] + }, + "value": { + "type": "string", + "description": "Represents a arbitrary String value" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "number_array" + ] + }, + "value": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + }, + "description": "Represents an array of numbers. This is basically used for\n\"one of the given numbers\" operations\neg: payment.method.amount = (1, 2, 3)" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "enum_variant_array" + ] + }, + "value": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Similar to NumberArray but for enum variants\neg: payment.method.cardtype = (debit, credit)" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "number_comparison_array" + ] + }, + "value": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NumberComparison" + }, + "description": "Like a number array but can include comparisons. Useful for\nconditions like \"500 < amount < 1000\"\neg: payment.amount = (> 500, < 1000)" + } + } + } + ], + "description": "Represents a value in the DSL", + "discriminator": { + "propertyName": "type" + } + }, "VoucherData": { "oneOf": [ { @@ -12163,7 +16738,7 @@ "type": "apiKey", "in": "header", "name": "api-key", - "description": "API keys are the most common method of authentication and can be obtained from the HyperSwitch dashboard." + "description": "Use the API key created under your merchant account from the HyperSwitch dashboard. API key is used to authenticate API requests from your merchant server only. Don't expose this key on a website or embed it in a mobile application." }, "ephemeral_key": { "type": "apiKey", @@ -12212,6 +16787,10 @@ "name": "Disputes", "description": "Manage disputes" }, + { + "name": "API Key", + "description": "Create and manage API Keys" + }, { "name": "Payouts", "description": "Create and manage payouts" @@ -12219,6 +16798,10 @@ { "name": "payment link", "description": "Create payment link" + }, + { + "name": "Routing", + "description": "Create and manage routing configurations" } ] } \ No newline at end of file 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/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/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 similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/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 similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js 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/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/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 similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json 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..b5002aeca8e7 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 @@ -56,7 +56,12 @@ "payment_method_types": [ { "payment_method_type": "credit", - "card_networks": ["Visa", "Mastercard"], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -64,7 +69,12 @@ }, { "payment_method_type": "debit", - "card_networks": ["Visa", "Mastercard"], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -190,6 +200,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": [ @@ -299,21 +321,6 @@ "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"] - } } } } 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/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/event.prerequest.js rename to postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js 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/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/event.prerequest.js rename to postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js 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/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json 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/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json 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/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/event.test.js index 93bd98b4ccb8..1e8dabda8275 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/event.test.js +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/event.test.js @@ -75,12 +75,12 @@ if (jsonData?.error?.type) { ); } -// Response body should have value "invalid_request" for "error type" -if (jsonData?.error?.message) { +// Response body should have value "connector error" for "error type" +if (jsonData?.error?.type) { pm.test( - "[POST]::/payments - Content check if value for 'error.message' matches 'Card Expired'", + "[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'", function () { - pm.expect(jsonData.error.message).to.eql("Card Expired"); + pm.expect(jsonData.error.type).to.eql("invalid_request"); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/.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 similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/.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 similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/.event.meta.json rename to 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 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/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/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 similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/response.json rename to 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 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - 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 - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/.event.meta.json rename to 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 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/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/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 similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/request.json rename to 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 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - 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 - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/response.json rename to 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 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/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json 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/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario7-Refund exceeds amount/Refunds - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario7-Refund exceeds amount/Refunds - Create/event.test.js index a195cd43879e..24327b0aad03 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario7-Refund exceeds amount/Refunds - Create/event.test.js +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario7-Refund exceeds amount/Refunds - Create/event.test.js @@ -50,10 +50,10 @@ if (jsonData?.error?.type) { // Response body should have value "invalid_request" for "error type" if (jsonData?.error?.message) { pm.test( - "[POST]::/payments - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'", + "[POST]::/payments - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'", function () { pm.expect(jsonData.error.message).to.eql( - "Refund amount exceeds the payment amount", + "The refund amount exceeds the amount captured", ); }, ); diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js index 6731d57fb694..b88beefec22e 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js @@ -50,10 +50,10 @@ if (jsonData?.error?.type) { // Response body should have value "invalid_request" for "error type" if (jsonData?.error?.message) { pm.test( - "[POST]::/payments - Content check if value for 'error.message' matches 'The payment has not succeeded yet. Please pass a successful payment to initiate refund'", + "[POST]::/payments - Content check if value for 'error.message' matches 'This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured'", function () { pm.expect(jsonData.error.message).to.eql( - "The payment has not succeeded yet. Please pass a successful payment to initiate refund", + "This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured", ); }, ); 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/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 index ac3f862e43f4..e7f91410f313 100644 --- 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 @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // 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'", + "[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"); }, ); } 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 index 21f054843897..e37391b78b5c 100644 --- 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 @@ -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/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 index a6976d95f69e..81ce7784e832 100644 --- 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 @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id - 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"); }, ); } 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 index b160ad9dc04b..9b9d104698f4 100644 --- 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 @@ -86,9 +86,9 @@ if (jsonData?.amount) { // 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'", + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("succeeded"); }, ); } 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 index b1d5ad5ebbf8..c2a8a615d1c2 100644 --- 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 @@ -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/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 index f87069589f0a..80343f371a60 100644 --- 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 @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments:id - 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"); }, ); } 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 index 255743af78c7..91c871e2160c 100644 --- 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 @@ -65,9 +65,9 @@ if (jsonData?.client_secret) { // 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'", + "[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("succeeded"); }, ); } 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 index 4fbefdb8494a..ef09f8071d62 100644 --- 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 @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments:id - 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"); }, ); } 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 index fa6deebe16a8..ea70c2aace0e 100644 --- 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 @@ -63,12 +63,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "processing" for "status" +// Response body should have value "partially_captured" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } 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 index 5e3ff0e70ad2..878ea581a938 100644 --- 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 @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "manual", "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/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 index b1b53a360e32..cc4a60507a89 100644 --- 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 @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "processing" for "status" +// Response body should have value "partially_captured" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'processing'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } 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/Scenario28-Create partially captured payment with refund/.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json new file mode 100644 index 000000000000..6626732a3cab --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json @@ -0,0 +1,12 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Capture", + "Payments - Retrieve", + "Refunds - Create", + "Refunds - Create-copy", + "Refunds - Retrieve Copy", + "Refunds - Validation should throw", + "Payments - Retrieve Copy" + ] +} 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/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.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/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js new file mode 100644 index 000000000000..96d98780785a --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js @@ -0,0 +1,105 @@ +// 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 "partially_captured" 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); + }, + ); +} + +// Response body should have value "0" for "amount_received" +if (jsonData?.amount_capturable) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_capturable' matches '0'", + function () { + pm.expect(jsonData.amount_capturable).to.eql(0); + }, + ); +} + diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json new file mode 100644 index 000000000000..8975575ca40e --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/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/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/response.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json 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/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.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/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/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/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json new file mode 100644 index 000000000000..4cbb79aa56af --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json @@ -0,0 +1,94 @@ +{ + "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", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/response.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js new file mode 100644 index 000000000000..3342e5b2530d --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js @@ -0,0 +1,85 @@ +// 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 "partially_captured" 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"); + }, + ); +} + +// Check if the "refunds" array exists +pm.test("Check if 'refunds' array exists", function() { + pm.expect(jsonData.refunds).to.be.an("array"); +}); + +// Check if there are exactly 2 items in the "refunds" array +pm.test("Check if there are 2 refunds", function() { + pm.expect(jsonData.refunds.length).to.equal(2); +}); + + + + diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json similarity index 86% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/request.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/request.json +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/response.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..89b1355575ae --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/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 "partially_captured" 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/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json similarity index 86% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/request.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/response.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} 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/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js similarity index 83% rename from postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js index c549d5d0c097..b0a888ae70d4 100644 --- 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/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js @@ -39,17 +39,12 @@ if (jsonData?.status) { ); } -// Response body should have value "540" for "amount" +// Response body should have value "6540" for "amount" if (jsonData?.status) { pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '540'", + "[POST]::/refunds - Content check if value for 'amount' matches '6540'", function () { - pm.expect(jsonData.amount).to.eql(540); + pm.expect(jsonData.amount).to.eql(2000); }, ); } - -// Validate the connector -pm.test("[POST]::/payments - connector", function () { - pm.expect(jsonData.connector).to.eql("checkout"); -}); diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json new file mode 100644 index 000000000000..ff371b247dbe --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json @@ -0,0 +1,42 @@ +{ + "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": 2000, + "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/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/response.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js new file mode 100644 index 000000000000..ccc9bf470227 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js @@ -0,0 +1,50 @@ +// 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 "6540" for "amount" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'amount' matches '4000'", + function () { + pm.expect(jsonData.amount).to.eql(4000); + }, + ); +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json new file mode 100644 index 000000000000..933f1a66edad --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json @@ -0,0 +1,42 @@ +{ + "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": 4000, + "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/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/response.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js similarity index 91% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/event.test.js rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js index 920a7c47f361..072e259d834a 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js @@ -39,12 +39,12 @@ if (jsonData?.status) { ); } -// Response body should have value "6540" for "amount" +// Response body should have value "2000" for "amount" if (jsonData?.status) { pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '540'", + "[POST]::/refunds - Content check if value for 'amount' matches '2000'", function () { - pm.expect(jsonData.amount).to.eql(540); + pm.expect(jsonData.amount).to.eql(2000); }, ); } 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/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json similarity index 100% rename from postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/response.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js new file mode 100644 index 000000000000..2f5de7240613 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js @@ -0,0 +1,41 @@ +// Validate status 2xx +pm.test("[POST]::/refunds - Status code is 4xx", function () { + pm.response.to.be.error; +}); + +// 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?.error.message) { + pm.test( + "[POST]::/refunds - Content check if value for 'message' matches 'The refund amount exceeds the amount captured'", + function () { + pm.expect(jsonData.error.message).to.eql("The refund amount exceeds the amount captured"); + }, + ); +} + diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json new file mode 100644 index 000000000000..ff371b247dbe --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json @@ -0,0 +1,42 @@ +{ + "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": 2000, + "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/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/response.json rename to postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json 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/bluesnap/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/bluesnap/Flow Testcases/QuickStart/Payment Connector - Create/request.json index ef3bdcd3aa67..4609a0100540 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/bluesnap/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -53,7 +53,25 @@ "payment_method_types": [ { "payment_method_type": "credit", - "card_networks": ["Visa", "Mastercard"], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, diff --git a/postman/collection-dir/checkout/.event.meta.json b/postman/collection-dir/checkout/.event.meta.json index eb871bbcb9bb..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/.event.meta.json +++ b/postman/collection-dir/checkout/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.prerequest.js", "event.test.js"] + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/.info.json b/postman/collection-dir/checkout/.info.json index d061408c8abb..4c3a1efad421 100644 --- a/postman/collection-dir/checkout/.info.json +++ b/postman/collection-dir/checkout/.info.json @@ -1,9 +1,10 @@ { "info": { - "_postman_id": "0da8c3be-4466-413e-9e72-81b21533423e", + "_postman_id": "9ab8f157-6b4b-430a-9ca8-34931682f988", "name": "Checkout Collection", "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": "25737662" + "_exporter_id": "25180378", + "_collection_link": "https://galactic-desert-365744.postman.co/workspace/postman-tests-for-all-connector~f274d40a-132c-47e3-8240-6793392ee4d1/collection/25180378-9ab8f157-6b4b-430a-9ca8-34931682f988?action=share&creator=25180378&source=collection_link" } } diff --git a/postman/collection-dir/checkout/.meta.json b/postman/collection-dir/checkout/.meta.json index d513035ce2d6..91b6a65c5bc6 100644 --- a/postman/collection-dir/checkout/.meta.json +++ b/postman/collection-dir/checkout/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Health check", "Flow Testcases"] + "childrenOrder": [ + "Health check", + "Flow Testcases" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/.meta.json b/postman/collection-dir/checkout/Flow Testcases/.meta.json index 023989e1e494..1bbce843680e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/.meta.json @@ -1,3 +1,7 @@ { - "childrenOrder": ["QuickStart", "Happy Cases", "Variation Cases"] + "childrenOrder": [ + "QuickStart", + "Happy Cases", + "Variation Cases" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/.meta.json index b2d6cbdc5495..bf036d3b7368 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/.meta.json @@ -1,5 +1,9 @@ { "childrenOrder": [ + "Scenario11-Save card flow", + "Scenario12-Don't Pass CVV for save card flow and verify success payment", + "Scenario13-Pass Invalid CVV for save card flow and verify failed payment", + "Scenario14-Save card payment with manual capture", "Scenario1-Create payment with confirm true", "Scenario2-Create payment with confirm false", "Scenario3-Create payment without PMD", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json index 69b505c6d863..60051ecca220 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Retrieve"] + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js index cb7a76331cd6..30eaca5c0ca0 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" 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 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index 144a35f773aa..4e96529fe9a9 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -30,11 +30,11 @@ "phone_country_code": "+1", "description": "Its my first payment request", "authentication_type": "no_three_ds", - "return_url": "https://duck.com", + "return_url": "https://google.com", "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4485040371536584", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/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/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/.meta.json index 69b505c6d863..60051ecca220 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Retrieve"] + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/request.json index f5f3112ab99b..197580ddffa4 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/request.json @@ -80,8 +80,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/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/3DS Payment/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } 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 - Cancel/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/request.json index 3a1d8aa178f2..f64e37a125a2 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/request.json @@ -23,8 +23,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/cancel", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "cancel"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } 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/Cancel After Partial Capture/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/request.json index 2c085d1319bd..b28dec99902e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/request.json @@ -80,8 +80,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/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/Cancel After Partial Capture/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/request.json index 2c085d1319bd..b28dec99902e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/request.json @@ -80,8 +80,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } 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/Refund After Partial Capture/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/request.json index 491b7d68feb6..ff371b247dbe 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } 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 - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/request.json index 2c085d1319bd..b28dec99902e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/request.json @@ -80,8 +80,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-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/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-copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/request.json index 38ea8b2b84e9..d10988d49b14 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/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 - 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/Retrieve After Partial Capture/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } 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 - 1/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } 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 - 2/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } 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/Payments - Capture - 3/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/request.json index 2c085d1319bd..b28dec99902e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/request.json @@ -80,8 +80,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/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/Successful Partial Capture and Refund/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/request.json index 38ea8b2b84e9..d10988d49b14 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } 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/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/request.json index 96cd9f45b0ff..9e0bc932afbe 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Create/event.test.js index b7c7c3384d99..74bc3f6ab783 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Create/event.test.js @@ -49,12 +49,12 @@ if (jsonData?.customer_id) { console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.'); }; -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" 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 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve-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/Payments - Retrieve-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/Scenario11-Save card flow/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve/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/Payments - Retrieve/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.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/.event.meta.json index 688c85746ef1..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/.event.meta.json @@ -1,5 +1,6 @@ { "eventOrder": [ + "event.prerequest.js", "event.test.js" ] } 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/Scenario11-Save card flow/Save card payments - Confirm/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Save card payments - Confirm/event.test.js index 22d88c07e50a..4f38b75d82fd 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Save card payments - Confirm/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Save card payments - Confirm/event.test.js @@ -63,12 +63,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" 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 'succeeded'", + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Payments - Retrieve/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/Scenario12-Don't Pass CVV for save card flow and verify success payment/Save card payments - Confirm/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Save card payments - Confirm/event.test.js index c5b17c6e79ad..82a20684b26c 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Save card payments - Confirm/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Save card payments - Confirm/event.test.js @@ -63,16 +63,16 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" 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 'succeeded'", + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } -// Response body should have value "adyen" for "connector" +// Response body should have value "checkout" for "connector" if (jsonData?.connector) { pm.test( "[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario13-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario13-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario13-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/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/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/.meta.json index 4bacbe0f555f..fc583d47a253 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/.meta.json @@ -6,8 +6,6 @@ "Save card payments - Create", "Save card payments - Confirm", "Payments - Capture", - "Payments - Retrieve-copy", - "Refunds - Create Copy", - "Refunds - Retrieve Copy" + "Payments - Retrieve-copy" ] } 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 - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Create/event.test.js index b7c7c3384d99..74bc3f6ab783 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Create/event.test.js @@ -49,12 +49,12 @@ if (jsonData?.customer_id) { console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.'); }; -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" 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 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } \ 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 - Retrieve-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-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 - 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/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve/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/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js index 8a41b0ef7e5d..bc42544064b8 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js @@ -63,12 +63,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" 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 'succeeded'", + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json index 98eb4829e4bd..cffc5ba2ed34 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json @@ -43,8 +43,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index a6a8150c2404..762826dac09d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -30,7 +30,7 @@ "phone_country_code": "+1", "description": "Its my first payment request", "authentication_type": "no_three_ds", - "return_url": "https://duck.com", + "return_url": "https://google.com", "payment_method": "card", "payment_method_data": { "card": { @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/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/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js index 2fd89553c9cc..71acfe97494c 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js @@ -63,12 +63,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" 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 'succeeded'", + "[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json index dd704a5de76c..cc9d7304aa36 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json @@ -53,8 +53,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json index e3b1e235042f..b28abd0c3090 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json @@ -67,8 +67,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/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/Scenario3-Create payment without PMD/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } 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 - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/request.json index 9fe257ed85e6..8975575ca40e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/request.json index 1c3a9e08ac33..3895e38ff26a 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/request.json @@ -43,8 +43,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json index 50cb0663b403..de5aa29f7b05 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/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/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/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/request.json index cceb2b55f0a7..8efb99d3c905 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/request.json index 5b606850fd2e..c4f2248a4f45 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario5-Create payment with Manual capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/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/Scenario5-Create payment with Manual capture/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } 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 - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/request.json index 9fe257ed85e6..8975575ca40e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/request.json index 5b606850fd2e..c4f2248a4f45 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario6-Create Partial Capture payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/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/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/Scenario6-Create Partial Capture payment/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/request.json index 3a1d8aa178f2..f64e37a125a2 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/request.json @@ -23,8 +23,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/cancel", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "cancel"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json index 5b606850fd2e..c4f2248a4f45 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario7-Void the payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/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/Scenario7-Void the payment/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/event.test.js index 2f020d7ff20d..4b3558a20cc5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" 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 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/request.json index 144a35f773aa..47f08f9f3721 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario8-Refund full payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/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/Scenario8-Refund full payment/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } 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/Scenario8-Refund full payment/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/request.json index 5f4c58816d58..5e306df7a552 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario8-Refund full payment/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/event.test.js index 2f020d7ff20d..4b3558a20cc5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" 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 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/request.json index 144a35f773aa..47f08f9f3721 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario9-Partial refund/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-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/Payments - Retrieve-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/Payments - Retrieve-copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/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/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/.event.meta.json index 0731450e6b25..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] } 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-copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/request.json index caed78185784..b56057fad5dc 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario9-Partial refund/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/.event.meta.json index 0731450e6b25..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] } 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/Happy Cases/Scenario9-Partial refund/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/request.json index 9fe125ce8ea4..d18aaf8befdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario9-Partial refund/Refunds - Retrieve-copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/request.json index 6ceefe5d24cd..4e4c66284978 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/request.json @@ -35,8 +35,13 @@ }, "url": { "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": ["{{baseUrl}}"], - "path": ["api_keys", ":merchant_id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], "variable": [ { "key": "merchant_id", diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/request.json index dcbf46ee5382..ffeea3410a4c 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -84,8 +84,12 @@ }, "url": { "raw": "{{baseUrl}}/accounts", - "host": ["{{baseUrl}}"], - "path": ["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/checkout/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 0f14659a5e8f..7ca3d82e793e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -56,7 +56,12 @@ "payment_method_types": [ { "payment_method_type": "credit", - "card_networks": ["Visa", "Mastercard"], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -64,7 +69,12 @@ }, { "payment_method_type": "debit", - "card_networks": ["Visa", "Mastercard"], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -94,8 +104,14 @@ }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": ["{{baseUrl}}"], - "path": ["account", ":account_id", "connectors"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], "variable": [ { "key": "account_id", diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/request.json index 07ffc4eedefc..872fac0738c4 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/QuickStart/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/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/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/request.json index ef0b213739db..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/request.json @@ -7,9 +7,20 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } 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/QuickStart/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/request.json index 5f4c58816d58..5e306df7a552 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/request.json index 9cbdbc8e22f8..ad281c58256a 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json index 6e9db26a339d..03e71d6c7eae 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/request.json index 418f77270d94..2d0e17e60e8a 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/request.json @@ -75,8 +75,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json index 0b35b7a4e92b..bd5526d6f86b 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Variation Cases/Scenario2-Confirming the payment without PMD/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/.meta.json index f2bee31ef66f..90b19864ee18 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Confirm"] + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/request.json index 98eb4829e4bd..cffc5ba2ed34 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/request.json @@ -43,8 +43,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/request.json index e3b1e235042f..b28abd0c3090 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/request.json @@ -67,8 +67,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/request.json index 6c934a9132cf..766a8330d6e8 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json index 5b606850fd2e..c4f2248a4f45 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/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/Scenario3-Capture greater amount/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/.meta.json index 6df7c0a19439..cafd3a6808e5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Capture"] + "childrenOrder": [ + "Payments - Create", + "Payments - Capture" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/request.json index 6c934a9132cf..766a8330d6e8 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/event.test.js index c48d8e2d054e..16c37817f0b1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" 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 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json index 144a35f773aa..47f08f9f3721 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Variation Cases/Scenario5-Void the success_slash_failure payment/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/.meta.json index 5ff1a7cb4e8e..afba25ab6d6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Cancel"] + "childrenOrder": [ + "Payments - Create", + "Payments - Cancel" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/request.json index 3a1d8aa178f2..f64e37a125a2 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/request.json @@ -23,8 +23,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/cancel", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "cancel"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/event.test.js index c48d8e2d054e..16c37817f0b1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" 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 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json index 09987daa71ec..06284e30aedc 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/event.test.js index 2f020d7ff20d..4b3558a20cc5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" 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 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/request.json index 144a35f773aa..47f08f9f3721 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/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/Payments - Retrieve/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/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/.event.meta.json index 0731450e6b25..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] } 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 exceeds amount/Refunds - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.test.js index 00f93b844c5b..d5f63c170ffe 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.test.js @@ -47,13 +47,13 @@ if (jsonData?.error?.type) { ); } -// Response body should have value "Refund amount exceeds the payment amount" for "message" +// Response body should have value "The refund amount exceeds the amount captured" for "message" if (jsonData?.error?.message) { pm.test( - "[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'", + "[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'", function () { pm.expect(jsonData.error.message).to.eql( - "Refund amount exceeds the payment amount", + "The refund amount exceeds the amount captured", ); }, ); diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/request.json index 10de81bed082..326319e8fdf4 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/request.json index 45feadeb3c38..758498447dd9 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/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/Payments - Retrieve/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/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/.event.meta.json index 0731450e6b25..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] } 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/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/request.json index 9fe125ce8ea4..d18aaf8befdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Health check/.meta.json b/postman/collection-dir/checkout/Health check/.meta.json index 579f9fd541ca..66ee7e50cab8 100644 --- a/postman/collection-dir/checkout/Health check/.meta.json +++ b/postman/collection-dir/checkout/Health check/.meta.json @@ -1,3 +1,5 @@ { - "childrenOrder": ["New Request"] + "childrenOrder": [ + "New Request" + ] } diff --git a/postman/collection-dir/checkout/Health check/New Request/.event.meta.json b/postman/collection-dir/checkout/Health check/New Request/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Health check/New Request/.event.meta.json +++ b/postman/collection-dir/checkout/Health check/New Request/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Health check/New Request/request.json b/postman/collection-dir/checkout/Health check/New Request/request.json index e40e93961785..4cc8d4b1a966 100644 --- a/postman/collection-dir/checkout/Health check/New Request/request.json +++ b/postman/collection-dir/checkout/Health check/New Request/request.json @@ -10,7 +10,11 @@ ], "url": { "raw": "{{baseUrl}}/health", - "host": ["{{baseUrl}}"], - "path": ["health"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] } } diff --git a/postman/collection-dir/hyperswitch/Hackathon/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/hyperswitch/Hackathon/QuickStart/Payment Connector - Create/request.json index 7eab20001651..7e3f37901512 100644 --- a/postman/collection-dir/hyperswitch/Hackathon/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/hyperswitch/Hackathon/QuickStart/Payment Connector - Create/request.json @@ -286,7 +286,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { diff --git a/postman/collection-dir/hyperswitch/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/hyperswitch/QuickStart/Payment Connector - Create/request.json index 7eab20001651..7e3f37901512 100644 --- a/postman/collection-dir/hyperswitch/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/hyperswitch/QuickStart/Payment Connector - Create/request.json @@ -286,7 +286,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { 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 8559af25e82c..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,10 +39,6 @@ }, "raw_json_formatted": { "client_secret": "{{client_secret}}", - "surcharge_details": { - "surcharge_amount": 5, - "tax_amount": 5 - }, "payment_method": "card", "payment_method_data": { "card": { 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 f7d813c34efd..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,6 +31,10 @@ "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", diff --git a/postman/collection-dir/prophetpay/.auth.json b/postman/collection-dir/prophetpay/.auth.json new file mode 100644 index 000000000000..915a28357900 --- /dev/null +++ b/postman/collection-dir/prophetpay/.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/prophetpay/.event.meta.json b/postman/collection-dir/prophetpay/.event.meta.json new file mode 100644 index 000000000000..eb871bbcb9bb --- /dev/null +++ b/postman/collection-dir/prophetpay/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.prerequest.js", "event.test.js"] +} diff --git a/postman/collection-dir/prophetpay/.info.json b/postman/collection-dir/prophetpay/.info.json new file mode 100644 index 000000000000..5c4524a2797a --- /dev/null +++ b/postman/collection-dir/prophetpay/.info.json @@ -0,0 +1,9 @@ +{ + "info": { + "_postman_id": "a553df38-fa33-4522-b029-1cd32821730e", + "name": "Prophetpay Postman Collection", + "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": "24206034" + } +} diff --git a/postman/collection-dir/prophetpay/.meta.json b/postman/collection-dir/prophetpay/.meta.json new file mode 100644 index 000000000000..91b6a65c5bc6 --- /dev/null +++ b/postman/collection-dir/prophetpay/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Health check", + "Flow Testcases" + ] +} diff --git a/postman/collection-dir/prophetpay/.variable.json b/postman/collection-dir/prophetpay/.variable.json new file mode 100644 index 000000000000..af2658909e76 --- /dev/null +++ b/postman/collection-dir/prophetpay/.variable.json @@ -0,0 +1,96 @@ +{ + "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": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "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" + } + ] +} diff --git a/postman/collection-dir/prophetpay/Flow Testcases/.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/.meta.json new file mode 100644 index 000000000000..5e5d7fb58811 --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["QuickStart", "Happy Cases"] +} diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/.meta.json new file mode 100644 index 000000000000..1563aa6b43a8 --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Scenario1-Create payment with confirm true", + "Scenario2-Create payment with confirm false" + ] +} diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json new file mode 100644 index 000000000000..69b505c6d863 --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create", "Payments - Retrieve"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/.event.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/.event.meta.json rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js new file mode 100644 index 000000000000..8d85991a308b --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/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 'requires_customer_action'", + function () { + pm.expect(jsonData.status).to.eql("requires_customer_action"); + }, + ); +} \ No newline at end of file diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json new file mode 100644 index 000000000000..2c8566c24282 --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -0,0 +1,47 @@ +{ + "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": 8000, + "currency": "USD", + "confirm": true, + "amount_to_capture": 8000, + "business_country": "US", + "customer_id": "not_a_rick_roll", + "return_url": "https://www.google.com", + "payment_method": "card_redirect", + "payment_method_type": "card_redirect", + "payment_method_data": { + "card_redirect": { + "card_redirect": {} + } + }, + "routing": { + "type": "single", + "data": "prophetpay" + } + } + }, + "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/Scenario22-Wallet-Wechatpay/Payments - Retrieve/response.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/response.json rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/.event.meta.json rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..02ab01502c1b --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/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 "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"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/request.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/request.json rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/response.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/response.json rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/.meta.json rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/prophetpay/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/prophetpay/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/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js b/postman/collection-dir/prophetpay/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/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..6c20960e9dcc --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js @@ -0,0 +1,74 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "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"); + }, + ); +} diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json new file mode 100644 index 000000000000..2370350edca9 --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json @@ -0,0 +1,70 @@ +{ + "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_redirect", + "payment_method_type": "card_redirect", + "payment_method_data": { + "card_redirect": { + "card_redirect": {} + } + }, + "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/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/response.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/response.json rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/event.test.js b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/event.test.js rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json new file mode 100644 index 000000000000..851a18115f97 --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -0,0 +1,40 @@ +{ + "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": 8000, + "currency": "USD", + "confirm": false, + "amount_to_capture": 8000, + "business_country": "US", + "customer_id": "not_a_rick_roll", + "return_url": "https://www.google.com", + "routing": { + "type": "single", + "data": "prophetpay" + } + } + }, + "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/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/response.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/response.json rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/prophetpay/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/prophetpay/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/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/event.test.js b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/event.test.js rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/request.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json similarity index 86% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/request.json rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/request.json +++ b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/response.json b/postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/response.json rename to postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/.meta.json new file mode 100644 index 000000000000..e3596ba357bc --- /dev/null +++ b/postman/collection-dir/prophetpay/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/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/.event.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/.event.meta.json rename to postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/.event.meta.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/event.test.js b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/event.test.js new file mode 100644 index 000000000000..4e27c5a50253 --- /dev/null +++ b/postman/collection-dir/prophetpay/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/prophetpay/Flow Testcases/QuickStart/API Key - Create/request.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/request.json new file mode 100644 index 000000000000..6ceefe5d24cd --- /dev/null +++ b/postman/collection-dir/prophetpay/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/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/response.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/response.json rename to postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json rename to postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js new file mode 100644 index 000000000000..7de0d5beb316 --- /dev/null +++ b/postman/collection-dir/prophetpay/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/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/request.json new file mode 100644 index 000000000000..3d331205b0a1 --- /dev/null +++ b/postman/collection-dir/prophetpay/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", + "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" + }, + "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/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/response.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/response.json rename to postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js new file mode 100644 index 000000000000..88e92d8d84a2 --- /dev/null +++ b/postman/collection-dir/prophetpay/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/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/request.json new file mode 100644 index 000000000000..09a2a5866ad7 --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -0,0 +1,90 @@ +{ + "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": "prophetpay", + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "{{connector_api_key}}", + "api_secret": "{{connector_api_secret}}", + "key1": "{{connector_key1}}" + }, + "business_country": "US", + "business_label": "default", + "test_mode": false, + "disabled": false, + "payment_methods_enabled": [ + { + "payment_method": "card_redirect", + "payment_method_types": [ + { + "payment_method_type": "card_redirect", + "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 + } + ] + } + ], + "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/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/response.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/response.json rename to postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json rename to postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/event.test.js b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/event.test.js new file mode 100644 index 000000000000..a6947db94c0b --- /dev/null +++ b/postman/collection-dir/prophetpay/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/prophetpay/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/request.json new file mode 100644 index 000000000000..11c9749e9589 --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/request.json @@ -0,0 +1,47 @@ +{ + "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", + "confirm": true, + "amount_to_capture": 10000, + "business_country": "US", + "customer_id": "not_a_rick_roll", + "return_url": "https://www.google.com", + "payment_method": "card_redirect", + "payment_method_type": "card_redirect", + "payment_method_data": { + "card_redirect": { + "card_redirect": {} + } + }, + "routing": { + "type": "single", + "data": "prophetpay" + } + } + }, + "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/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/response.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/response.json rename to postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/.event.meta.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/event.test.js b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..d0a02af74367 --- /dev/null +++ b/postman/collection-dir/prophetpay/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/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/request.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/request.json new file mode 100644 index 000000000000..ef0b213739db --- /dev/null +++ b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/request.json @@ -0,0 +1,22 @@ +{ + "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/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/response.json b/postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/response.json rename to postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/response.json diff --git a/postman/collection-dir/prophetpay/Health check/.meta.json b/postman/collection-dir/prophetpay/Health check/.meta.json new file mode 100644 index 000000000000..579f9fd541ca --- /dev/null +++ b/postman/collection-dir/prophetpay/Health check/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["New Request"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/.event.meta.json b/postman/collection-dir/prophetpay/Health check/New Request/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/.event.meta.json rename to postman/collection-dir/prophetpay/Health check/New Request/.event.meta.json diff --git a/postman/collection-dir/prophetpay/Health check/New Request/event.test.js b/postman/collection-dir/prophetpay/Health check/New Request/event.test.js new file mode 100644 index 000000000000..b490b8be090f --- /dev/null +++ b/postman/collection-dir/prophetpay/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/prophetpay/Health check/New Request/request.json b/postman/collection-dir/prophetpay/Health check/New Request/request.json new file mode 100644 index 000000000000..7ddcfbd2b724 --- /dev/null +++ b/postman/collection-dir/prophetpay/Health check/New Request/request.json @@ -0,0 +1,8 @@ +{ + "method": "GET", + "url": { + "raw": "{{baseUrl}}/health", + "host": ["{{baseUrl}}"], + "path": ["health"] + } +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/response.json b/postman/collection-dir/prophetpay/Health check/New Request/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/response.json rename to postman/collection-dir/prophetpay/Health check/New Request/response.json diff --git a/postman/collection-dir/prophetpay/event.prerequest.js b/postman/collection-dir/prophetpay/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/prophetpay/event.test.js b/postman/collection-dir/prophetpay/event.test.js new file mode 100644 index 000000000000..fb52caec30fc --- /dev/null +++ b/postman/collection-dir/prophetpay/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/stripe/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json index 62945fedcfaa..bfeee020b5d2 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json @@ -2,14 +2,18 @@ "childrenOrder": [ "Scenario1-Create payment with confirm true", "Scenario2-Create payment with confirm false", + "Scenario2a-Create payment with confirm false card holder name null", + "Scenario2b-Create payment with confirm false card holder name empty", "Scenario3-Create payment without PMD", "Scenario4-Create payment with Manual capture", + "Scenario4a-Create payment with manual_multiple capture", "Scenario5-Void the payment", "Scenario6-Create 3DS payment", "Scenario7-Create 3DS payment with confrm false", + "Scenario8-Create a failure card payment with confirm true", "Scenario9-Refund full payment", - "Scenario10-Partial refund", - "Scenario11-Create a mandate and recurring payment", + "Scenario9a-Partial refund", + "Scenario10-Create a mandate and recurring payment", "Scenario11-Refund recurring payment", "Scenario12-BNPL-klarna", "Scenario13-BNPL-afterpay", @@ -19,9 +23,13 @@ "Scenario17-Bank Redirect-eps", "Scenario18-Bank Redirect-giropay", "Scenario19-Bank Transfer-ach", - "Scenario19-Bank Debit-ach", - "Scenario22-Wallet-Wechatpay", + "Scenario20-Bank Debit-ach", + "Scenario21-Wallet-Wechatpay", "Scenario22- Update address and List Payment method", - "Scenario23- Update Amount" + "Scenario23- Update Amount", + "Scenario24-Add card flow", + "Scenario25-Don't Pass CVV for save card flow and verifysuccess payment", + "Scenario26-Save card payment with manual capture", + "Scenario27-Create payment without customer_id and with billing address and shipping 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..93bcc23194fe 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", @@ -41,7 +41,7 @@ "card": { "card_number": "4242424242424242", "card_exp_month": "01", - "card_exp_year": "24", + "card_exp_year": "26", "card_holder_name": "joseph Doe", "card_cvc": "123" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/request.json similarity index 97% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/request.json index a5c9391cf748..3ffbe03a6058 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/request.json @@ -98,8 +98,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring 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/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/request.json similarity index 96% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/request.json index 01f47678beab..613e9148f787 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/request.json @@ -61,8 +61,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Create/request.json index 599c708ba732..9df18b5e8863 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Create/request.json @@ -86,8 +86,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario11-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } 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..fe8a73d4581a 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", @@ -61,8 +61,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario11-Refund recurring payment/Refunds - Create Copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/request.json index 5f4c58816d58..5e306df7a552 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", 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/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js index dc69bd52a500..f92fba3d0440 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js @@ -63,26 +63,6 @@ if (jsonData?.client_secret) { ); } -// 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 "succeeded" for "status" if (jsonData?.status) { pm.test( @@ -92,12 +72,3 @@ if (jsonData?.status) { }, ); } - -// Response body should have "connector_transaction_id" -pm.test( - "[POST]::/payments - Content check if 'connector_transaction_id' exists", - function () { - pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be - .true; - }, -); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json index ec45ef29bb64..540a2fa19461 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json @@ -38,13 +38,41 @@ } }, "raw_json_formatted": { - "client_secret": "{{client_secret}}" + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "Joseph Doe", + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } } }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json index 0731450e6b25..4ac527d834af 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js index 55dc35b91280..0444324000a6 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "requires_confirmation" for "status" +// Response body should have value "requires_payment_method" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'", + "[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/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index 4105bd1a869b..b28abd0c3090 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -32,16 +32,6 @@ "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", @@ -51,7 +41,7 @@ "state": "California", "zip": "94122", "country": "US", - "first_name": "sundari" + "first_name": "PiX" } }, "shipping": { @@ -63,7 +53,7 @@ "state": "California", "zip": "94122", "country": "US", - "first_name": "sundari" + "first_name": "PiX" } }, "statement_descriptor_name": "joseph", @@ -72,17 +62,17 @@ "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"] + "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/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js index 53f98b7f7f4d..aced67dbfb78 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js @@ -69,23 +69,3 @@ if (jsonData?.status) { }, ); } - -// 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/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/.meta.json new file mode 100644 index 000000000000..60051ecca220 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/request.json similarity index 95% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/request.json index 9612b4909870..eda0801c9cc1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/request.json @@ -23,7 +23,9 @@ "confirm": true, "business_label": "default", "capture_method": "automatic", - "connector": ["stripe"], + "connector": [ + "stripe" + ], "customer_id": "klarna", "capture_on": "2022-09-10T10:11:12Z", "authentication_type": "three_ds", @@ -83,8 +85,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario20-Bank Debit-ach/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/request.json similarity index 92% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/request.json index 9189e4dd8529..42d3653ad96a 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/request.json @@ -50,8 +50,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/request.json similarity index 95% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/request.json index 731eeaf14001..daffd2ab9ec1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/request.json @@ -49,8 +49,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario21-Wallet-Wechatpay/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json index 060c693c7e19..fed600e09cd9 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json @@ -32,15 +32,6 @@ "disabled": true } ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", "host": [ diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json index 060c693c7e19..fed600e09cd9 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json @@ -32,15 +32,6 @@ "disabled": true } ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", "host": [ 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 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..60e56bb581cc 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", @@ -41,7 +41,7 @@ "card": { "card_number": "4242424242424242", "card_exp_month": "01", - "card_exp_year": "24", + "card_exp_year": "26", "card_holder_name": "joseph Doe", "card_cvc": "123" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/.meta.json new file mode 100644 index 000000000000..343d847414ad --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create with confirm true", "Payments - Confirm"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..018bdf6fc328 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/event.test.js @@ -0,0 +1,33 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 400", function () { + pm.response.to.be.error; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + + +// Response body should have appropriatae error message +if (jsonData?.message) { + pm.test( + "Content check if appropriate error message is present", + function () { + pm.expect(jsonData.message).to.eql("You cannot confirm this payment because it has status requires_customer_action"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/request.json new file mode 100644 index 000000000000..1ce1d61eaa15 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/request.json @@ -0,0 +1,79 @@ +{ + "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}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id", "confirm"], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/event.test.js new file mode 100644 index 000000000000..39cbb3ee90e9 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/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 "requires_customer_action" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - 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; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/request.json new file mode 100644 index 000000000000..d14ae6582c8a --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/request.json @@ -0,0 +1,91 @@ +{ + "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", + "business_country": "US", + "business_label": "default", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4000000000003063", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..f92fba3d0440 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js @@ -0,0 +1,74 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/request.json new file mode 100644 index 000000000000..604ac54144dd --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/request.json @@ -0,0 +1,85 @@ +{ + "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": null, + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/request.json new file mode 100644 index 000000000000..b28abd0c3090 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/request.json @@ -0,0 +1,78 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..aced67dbfb78 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/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/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..9147db9461b1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js @@ -0,0 +1,74 @@ +// 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", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} \ No newline at end of file diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/request.json new file mode 100644 index 000000000000..371ef616fe4e --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/request.json @@ -0,0 +1,85 @@ +{ + "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": "", + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_payment_method" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_payment_method"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/request.json new file mode 100644 index 000000000000..b28abd0c3090 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/request.json @@ -0,0 +1,78 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..3a1204713587 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/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"); + }, + ); +} \ No newline at end of file diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] 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/Scenario4a-Create payment with manual_multiple capture/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-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/Scenario4a-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/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-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/Scenario4a-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/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/request.json new file mode 100644 index 000000000000..8975575ca40e --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple 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/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-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/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-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/Scenario4a-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/Scenario4a-Create payment with manual_multiple capture/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/request.json new file mode 100644 index 000000000000..0fbd6a4dcdd5 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/request.json @@ -0,0 +1,92 @@ +{ + "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/Scenario4a-Create payment with manual_multiple capture/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-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/Scenario4a-Create payment with manual_multiple capture/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-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/Scenario4a-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/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple 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/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-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/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/.meta.json new file mode 100644 index 000000000000..60051ecca220 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/event.test.js 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/Scenario8-Create a failure card payment with confirm true/Payments - Create/request.json similarity index 94% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/request.json index 150139b8e104..6d1bc5e0a0dd 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/Scenario8-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", @@ -41,7 +41,7 @@ "card": { "card_number": "4000000000009995", "card_exp_month": "01", - "card_exp_year": "24", + "card_exp_year": "26", "card_holder_name": "joseph Doe", "card_cvc": "123" } @@ -87,8 +87,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/Scenario8-Create a failure card payment with confirm true/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card 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/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/event.test.js new file mode 100644 index 000000000000..c48d8e2d054e --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/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/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/request.json new file mode 100644 index 000000000000..b5f464abc14b --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/request.json @@ -0,0 +1,92 @@ +{ + "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": "+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/Scenario9a-Partial refund/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..0652a2d92fd4 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/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/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/request.json similarity index 90% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/request.json index caed78185784..b56057fad5dc 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/event.test.js 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/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/request.json similarity index 100% rename from postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/request.json similarity index 84% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} 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/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/request.json similarity index 84% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 474b4d1e1eaf..69e35cff273c 100644 --- a/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -211,7 +211,12 @@ "maximum_amount": 68607706, "recurring_enabled": true, "installment_payment_enabled": true, - "card_networks": ["Visa", "Mastercard"] + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"] } ] }, @@ -224,7 +229,12 @@ "maximum_amount": 68607706, "recurring_enabled": true, "installment_payment_enabled": true, - "card_networks": ["Visa", "Mastercard"] + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"] } ] }, @@ -298,7 +308,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/.meta.json new file mode 100644 index 000000000000..7dd4c0a0c214 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/.meta.json @@ -0,0 +1,8 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Capture", + "Payments - Retrieve", + "Refunds - Create" + ] +} \ No newline at end of file diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/event.test.js new file mode 100644 index 000000000000..f560d84ea730 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/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/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/request.json new file mode 100644 index 000000000000..9fe257ed85e6 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/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/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/response.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/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/Scenario10-Partial refund/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/request.json similarity index 97% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/request.json index 2363c62ff27f..90b6e3bd0385 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/request.json @@ -21,9 +21,8 @@ "amount": 6540, "currency": "USD", "confirm": true, - "capture_method": "automatic", + "capture_method": "manual", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..92af2088cd92 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/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 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/request.json rename to postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/event.test.js new file mode 100644 index 000000000000..07721f97af31 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/event.test.js @@ -0,0 +1,58 @@ +// Validate status 4xx +pm.test("[POST]::/refunds - Status code is 4xx", function () { + pm.response.to.be.error; +}); + +// 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 "error" +pm.test( + "[POST]::/payments/:id/confirm - Content check if 'error' exists", + function () { + pm.expect(typeof jsonData.error !== "undefined").to.be.true; + }, +); + +// Response body should have value "invalid_request" for "error type" +if (jsonData?.error?.type) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'", + function () { + pm.expect(jsonData.error.type).to.eql("invalid_request"); + }, + ); +} + +// Response body should have value "The refund amount exceeds the amount captured" for "error message" +if (jsonData?.error?.type) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'", + function () { + pm.expect(jsonData.error.message).to.eql("The refund amount exceeds the amount captured"); + }, + ); +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/request.json similarity index 97% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/request.json index 9fe125ce8ea4..5f4c58816d58 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/request.json @@ -19,7 +19,7 @@ }, "raw_json_formatted": { "payment_id": "{{payment_id}}", - "amount": 540, + "amount": 6540, "reason": "Customer returned product", "refund_type": "instant", "metadata": { diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json index 841485a0a048..ed2324e03082 100644 --- a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json +++ b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json @@ -27,14 +27,18 @@ } ], "url": { - "raw": "{{baseUrl}}/accounts/list", - "host": ["{{baseUrl}}"], - "path": ["accounts", "list"], + "raw": "{{baseUrl}}/accounts/list?organization_id={{organization_id}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + "list" + ], "query": [ { "key": "organization_id", - "value": "{{organization_id}}", - "disabled": false + "value": "{{organization_id}}" } ], "variable": [ 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/stripe/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/stripe/QuickStart/Payment Connector - Create/request.json index 9d742acdf3c6..3a281d4004a9 100644 --- a/postman/collection-dir/stripe/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/stripe/QuickStart/Payment Connector - Create/request.json @@ -291,7 +291,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { diff --git a/postman/collection-dir/stripe/QuickStart/Payment Connector - Update/request.json b/postman/collection-dir/stripe/QuickStart/Payment Connector - Update/request.json index ef812ef5d172..766fd2508190 100644 --- a/postman/collection-dir/stripe/QuickStart/Payment Connector - Update/request.json +++ b/postman/collection-dir/stripe/QuickStart/Payment Connector - Update/request.json @@ -288,7 +288,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/.meta.json index 6014e253b0a6..949d82095ec1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/.meta.json @@ -2,11 +2,13 @@ "childrenOrder": [ "Scenario1-Create payment with confirm true", "Scenario2-Create payment with confirm false", - "Scenario3-Create payment without PMD", - "Scenario4-Create 3DS payment", - "Scenario5-Create 3DS payment with confrm false", - "Scenario6-Refund full payment", + "Scenario3-Create payment with confirm false card holder name null", + "Scenario3-Create payment with confirm false card holder name empty", + "Scenario4-Create payment without PMD", + "Scenario5-Create 3DS payment", + "Scenario6-Create 3DS payment with confrm false", + "Scenario7-Refund full payment", "Scenario8-Bank Redirect-Ideal", - "Scenario11-Bank Redirect-giropay" + "Scenario9-Bank Redirect-giropay" ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json index 0731450e6b25..4ac527d834af 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js b/postman/collection-dir/trustpay/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/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json index d999cf2d6491..540a2fa19461 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json @@ -38,6 +38,16 @@ } }, "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}}", "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", @@ -55,8 +65,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json index 0731450e6b25..4ac527d834af 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js index 55dc35b91280..0444324000a6 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "requires_confirmation" for "status" +// Response body should have value "requires_payment_method" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'", + "[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/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index 119353069bd9..b28abd0c3090 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -32,16 +32,6 @@ "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", @@ -77,8 +67,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..9147db9461b1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js @@ -0,0 +1,74 @@ +// 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", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} \ No newline at end of file diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/request.json new file mode 100644 index 000000000000..371ef616fe4e --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/request.json @@ -0,0 +1,85 @@ +{ + "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": "", + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/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/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/request.json new file mode 100644 index 000000000000..b28abd0c3090 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/request.json @@ -0,0 +1,78 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..3a1204713587 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/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"); + }, + ); +} \ No newline at end of file diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/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/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..f92fba3d0440 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js @@ -0,0 +1,74 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/request.json new file mode 100644 index 000000000000..604ac54144dd --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/request.json @@ -0,0 +1,85 @@ +{ + "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": null, + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/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/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/request.json new file mode 100644 index 000000000000..b28abd0c3090 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/request.json @@ -0,0 +1,78 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..aced67dbfb78 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/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/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/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/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json index d30e00868a59..8ff62125d1ad 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json @@ -65,8 +65,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json index e3b1e235042f..b28abd0c3090 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json @@ -67,8 +67,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/.meta.json index 69b505c6d863..60051ecca220 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Retrieve"] + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/request.json index 7dd4aac02cef..80661beddf2d 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/request.json @@ -101,8 +101,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/request.json index 61fcdaec2d2e..6f4d51c5945e 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/.event.meta.json index 0731450e6b25..4ac527d834af 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/event.prerequest.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/request.json index 9fef5309c3a9..fb1c8124ca7e 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/request.json @@ -46,7 +46,7 @@ "card_number": "5200000000000015", "card_exp_month": "03", "card_exp_year": "2030", - "card_holder_name": "", + "card_holder_name": "John Doe", "card_cvc": "737", "card_issuer": "", "card_network": "Visa" @@ -68,8 +68,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/request.json index 3b4c8f74e7f5..0eb2d9f30cf8 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/request.json @@ -78,8 +78,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/request.json index 61fcdaec2d2e..6f4d51c5945e 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/request.json index 94473ddb1ec2..c3dddef3a2d5 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/request.json @@ -100,8 +100,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/request.json index 5f4c58816d58..5e306df7a552 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/request.json similarity index 93% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/request.json index 10b88b517486..c4c248b3bddc 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/request.json @@ -57,8 +57,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/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/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/request.json similarity index 97% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/request.json index 0b0c56d26601..87d07a6016b3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/request.json @@ -81,8 +81,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/request.json similarity index 86% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/request.json index 61fcdaec2d2e..6f4d51c5945e 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/request.json similarity index 93% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/request.json index caf10ddd35db..3f8a95e59bb2 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/request.json @@ -57,8 +57,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/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/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/request.json similarity index 97% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/request.json index 0b0c56d26601..87d07a6016b3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/request.json @@ -81,8 +81,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["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/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..9053ddab13be --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/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 "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"); + }, + ); +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/request.json similarity index 86% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/request.json index 61fcdaec2d2e..6f4d51c5945e 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/.meta.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/.meta.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/event.test.js diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/request.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/.event.meta.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/request.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/.event.meta.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/request.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} 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/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/.event.meta.json rename to postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json 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..6b4757c18c39 --- /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": "sdk-test-app.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/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/wise/Health check/Health/.event.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/wise/Health check/Health/.event.meta.json 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..91a03afa47c8 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\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}}}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "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,168 +6858,134 @@ } ], "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 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(\"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 {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id 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);", + "// 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 {{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;", - "});", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "// 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\");", - "});", + "// 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\");", + " },", + " );", + "}", "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "// 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;", + " },", + ");", "", - "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 \"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" } @@ -6978,126 +7000,90 @@ } ], "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": "Save card 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 \"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" } @@ -7122,45 +7108,139 @@ "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": "{\"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", + "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": "Save card payments - Confirm", + "name": "Refunds - Retrieve Copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - 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/:id/confirm - 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) {}", + "", + "// 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/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7209,70 +7289,22 @@ " );", "}", "", - "// Response body should have value \"failed\" for \"status\"", + "// Response body should have value \"requires_payment_method\" 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'\",", - " 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'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.error_code).to.eql(\"24\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", - "", - "// 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": [ { @@ -7291,51 +7323,45 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" + "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/: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": "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();", "});", "", @@ -7384,47 +7410,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 'failed'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", 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 \"6540\" for \"amount\"", - "if (jsonData?.amount) {", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", " },", " );", "}", "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", " 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);", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", @@ -7435,130 +7455,26 @@ } ], "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ + "auth": { + "type": "apikey", + "apikey": [ { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with confirm true", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "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" - } - } - ], - "request": { "method": "POST", "header": [ { @@ -7577,18 +7493,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\":\"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", + "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": [] }, @@ -7661,12 +7586,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'\",", + " \"[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\");", " },", " );", "}", @@ -7714,7 +7639,7 @@ ] }, { - "name": "Scenario2-Create payment with confirm false", + "name": "Scenario16-Bank debit-ach", "item": [ { "name": "Payments - Create", @@ -7785,12 +7710,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[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\");", " },", " );", "}", @@ -7819,7 +7744,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_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\":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", @@ -7906,6 +7831,16 @@ " );", "}", "", + "// 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(", @@ -7915,6 +7850,16 @@ " },", " );", "}", + "", + "// 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" @@ -7960,7 +7905,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" + "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", @@ -8106,7 +8051,7 @@ ] }, { - "name": "Scenario3-Create payment without PMD", + "name": "Scenario17-Bank debit-Bacs", "item": [ { "name": "Payments - Create", @@ -8211,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\":\"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\":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", @@ -8298,12 +8243,32 @@ " );", "}", "", - "// Response body should have value \"succeeded\" 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 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// 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 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\");", " },", " );", "}", @@ -8352,7 +8317,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\":\"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", @@ -8445,12 +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 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -8498,7 +8463,7 @@ ] }, { - "name": "Scenario4-Create payment with Manual capture", + "name": "Scenario18-Bank Redirect-Trustly", "item": [ { "name": "Payments - Create", @@ -8569,12 +8534,12 @@ " );", "}", "", - "// Response body should have value \"requires_capture\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -8603,7 +8568,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\":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", @@ -8619,20 +8584,20 @@ "response": [] }, { - "name": "Payments - Capture", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - 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/:id/capture - Content-Type is application/json\",", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", " function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", @@ -8641,7 +8606,7 @@ ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -8690,32 +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/capture - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// 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'\",", + "// 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.amount).to.eql(6540);", + " pm.expect(jsonData.payment_method_type).to.eql(\"trustly\");", " },", " );", "}", "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", @@ -8726,6 +8700,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -8744,17 +8738,17 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "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/capture", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "capture" + "confirm" ], "variable": [ { @@ -8764,7 +8758,7 @@ } ] }, - "description": "To capture the funds for an uncaptured 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": [] }, @@ -8837,12 +8831,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 - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -8861,7 +8855,7 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], @@ -8869,6 +8863,12 @@ "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", @@ -8884,7 +8884,7 @@ ] }, { - "name": "Scenario5-Void the payment", + "name": "Scenario19-Add card flow", "item": [ { "name": "Payments - Create", @@ -8893,78 +8893,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 +8967,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,143 +8983,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", + "method": "GET", "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", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "cancel" + "customers", + ":customer_id", + "payment_methods" ], - "variable": [ + "query": [ { - "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\",", - " );", + "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": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card 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.');", + "};", + "", + "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\":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", + "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": "Save card 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(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -9190,12 +9254,23 @@ " );", "}", "", - "// Response body should have value \"cancelled\" 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 'cancelled'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " 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\");", " },", " );", "}", @@ -9206,27 +9281,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\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" + }, "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": [ { @@ -9236,36 +9339,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": "Scenario6-Create 3DS payment", - "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.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();", "});", "", @@ -9314,139 +9412,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 - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"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", - "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();", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", "});", "", - "// 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.\",", + "// 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);", + " },", " );", "}", "", - "// 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 \"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 \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -9490,90 +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": "Scenario7-Create 3DS payment with confrm false", - "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);", + "// 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_confirmation\" for \"status\"", + "// Response body should have value \"540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " 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" @@ -9599,46 +9584,38 @@ "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\"}}" + "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", + "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": "Payments - Confirm", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - 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/: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(\"[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", @@ -9647,63 +9624,38 @@ " 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.\",", - " );", - "}", - "", - "// 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/confirm - 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);", " },", " );", "}", - "", - "// 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;", - " },", - ");", "" ], "type": "text/javascript" @@ -9711,147 +9663,162 @@ } ], "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}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" + "refunds", + ":id" ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund 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 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": "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", + "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.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": "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.');", + "}" ], "type": "text/javascript" } @@ -9866,58 +9833,822 @@ } ], "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" - } - ], - "variable": [ + "key": "accepted_country", + "value": "co", + "disabled": true + }, { - "key": "id", + "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": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card 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.');", + "};", + "", + "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\":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", + "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": "Save card 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 \"failed\" 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'\",", + " 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": [ + { + "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", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "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" + "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 \"Succeeded\" 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'\",", + " 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": "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "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.');", + "}" + ], + "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": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card 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.');", + "};", + "", + "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\":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", + "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": "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": [ @@ -10540,12 +11426,12 @@ " );", "}", "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.message) {", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Card Expired'\",", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Card Expired\");", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", " },", " );", "}", @@ -12724,10 +13610,10 @@ "// Response body should have value \"invalid_request\" for \"error type\"", "if (jsonData?.error?.message) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'\",", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", " function () {", " pm.expect(jsonData.error.message).to.eql(", - " \"Refund amount exceeds the payment amount\",", + " \"The refund amount exceeds the amount captured\",", " );", " },", " );", @@ -13073,10 +13959,10 @@ "// Response body should have value \"invalid_request\" for \"error type\"", "if (jsonData?.error?.message) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'The payment has not succeeded yet. Please pass a successful payment to initiate refund'\",", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured'\",", " function () {", " pm.expect(jsonData.error.message).to.eql(", - " \"The payment has not succeeded yet. Please pass a successful payment to initiate refund\",", + " \"This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured\",", " );", " },", " );", @@ -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 index 01524d91953d..752b77dcd150 100644 --- a/postman/collection-json/bankofamerica.postman_collection.json +++ b/postman/collection-json/bankofamerica.postman_collection.json @@ -2391,9 +2391,9 @@ "// 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'\",", + " \"[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\");", " },", " );", "}", @@ -2431,7 +2431,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", @@ -2518,9 +2518,9 @@ "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - 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\");", " },", " );", "}", @@ -2682,7 +2682,7 @@ "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\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"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", @@ -2792,9 +2792,9 @@ "// 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'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -2957,9 +2957,9 @@ "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - 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\");", " },", " );", "}", @@ -3221,9 +3221,9 @@ "// 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'\",", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3377,9 +3377,9 @@ "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - 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\");", " },", " );", "}", @@ -3532,7 +3532,7 @@ "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\"}}" + "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\":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\":\"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", @@ -3619,12 +3619,12 @@ " );", "}", "", - "// Response body should have value \"processing\" for \"status\"", + "// Response body should have value \"partially_captured\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -3766,12 +3766,12 @@ " );", "}", "", - "// Response body should have value \"processing\" for \"status\"", + "// Response body should have value \"partially_captured\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", diff --git a/postman/collection-json/bluesnap.postman_collection.json b/postman/collection-json/bluesnap.postman_collection.json index 82af7ce7bb6c..fa6c9258b8d0 100644 --- a/postman/collection-json/bluesnap.postman_collection.json +++ b/postman/collection-json/bluesnap.postman_collection.json @@ -449,7 +449,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"bluesnap\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\"},\"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\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"test_mode\":false,\"disabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"bluesnap\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\"},\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"test_mode\":false,\"disabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -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);", " },", @@ -3838,12 +3838,923 @@ " 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);", + "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\":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", + "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": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'bluesnap'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"bluesnap\");", + " },", + " );", + "}", + "", + "" + ], + "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\":\"card\",\"payment_token\":\"{{payment_token}}\"}" + }, + "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 \"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\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"bluesnap\");", + "});", + "", + "// 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" + } + } + ], + "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": "Scenario28-Create partially captured payment with refund", + "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\",\"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 \"partially_captured\" 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);", + " },", + " );", + "}", + "", + "// Response body should have value \"0\" for \"amount_received\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_capturable' matches '0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "", + "" + ], + "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 \"partially_captured\" 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": "Refunds - Create", + "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 \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '4000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(4000);", + " },", + " );", + "}", + "" + ], + "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\":4000,\"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 - 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 {{customer_id}}, as jsonData.customer_id 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 '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 '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(2000);", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -3868,46 +4779,38 @@ "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": "{\"payment_id\":\"{{payment_id}}\",\"amount\":2000,\"reason\":\"Customer returned product\",\"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": "Save card payments - Confirm", + "name": "Refunds - Retrieve Copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - 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/: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(\"[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", @@ -3916,60 +4819,117 @@ " 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,", + "// 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\");", + " },", " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + "}", + "", + "// Response body should have value \"2000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '2000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(2000);", + " },", " );", "}", + "" + ], + "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 - Validation should throw", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + "// 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 {{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/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) {", + "if (jsonData?.error.message) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'bluesnap'\",", + " \"[POST]::/refunds - Content check if value for 'message' matches 'The refund amount exceeds the amount captured'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"bluesnap\");", + " pm.expect(jsonData.error.message).to.eql(\"The refund amount exceeds the amount captured\");", " },", " );", "}", @@ -3981,26 +4941,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": [ { @@ -4019,32 +4959,23 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":2000,\"reason\":\"Customer returned product\",\"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": "Payments - Retrieve Copy", "event": [ { "listen": "test", @@ -4112,50 +5043,29 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"partially_captured\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - 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\");", " },", " );", "}", "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"bluesnap\");", + "// Check if the \"refunds\" array exists", + "pm.test(\"Check if 'refunds' array exists\", function() {", + " pm.expect(jsonData.refunds).to.be.an(\"array\");", "});", "", - "// 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);", - " },", - " );", - "}", + "// Check if there are exactly 2 items in the \"refunds\" array", + "pm.test(\"Check if there are 2 refunds\", function() {", + " pm.expect(jsonData.refunds.length).to.equal(2);", + "});", + "", + "", "", - "// 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" @@ -6596,9 +7506,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 +7653,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..d510b1c2a17f 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -424,7 +424,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"checkout\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"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\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":false,\"installment_payment_enabled\":false}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"checkout\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"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\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":false,\"installment_payment_enabled\":false}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -559,6 +559,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -639,7 +650,7 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], @@ -647,6 +658,12 @@ "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", @@ -730,7 +747,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -942,12 +961,12 @@ " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", "};", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"processing\" 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 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}" @@ -993,6 +1012,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -1416,12 +1446,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/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -1508,6 +1538,17 @@ { "name": "Payments - Retrieve-copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -1662,6 +1703,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": { @@ -2226,16 +2278,16 @@ " );", "}", "", - "// Response body should have value \"succeeded\" 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 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", - "// Response body should have value \"adyen\" for \"connector\"", + "// Response body should have value \"checkout\" for \"connector\"", "if (jsonData?.connector) {", " pm.test(", " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", @@ -2317,6 +2369,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -2950,6 +3013,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -3164,12 +3238,12 @@ " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", "};", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"processing\" 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 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}" @@ -3215,6 +3289,17 @@ { "name": "Payments - Retrieve", "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 +3887,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 +3924,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);", " },", " );", "}", @@ -3895,6 +3980,17 @@ { "name": "Payments - Retrieve-copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -3964,9 +4060,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\");", " },", " );", "}", @@ -3997,211 +4093,11 @@ "}", "", "// 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" - } - } - ], - "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 '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) {", + "if (jsonData?.amount) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -4220,23 +4116,29 @@ } ], "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": [] } @@ -4314,12 +4216,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"processing\" 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 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -4379,7 +4281,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4485040371536584\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4397,6 +4299,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -4671,7 +4584,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4758,12 +4671,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/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -4874,6 +4787,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -5250,12 +5174,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/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -5366,6 +5290,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -5929,9 +5864,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\");", " },", " );", "}", @@ -6022,6 +5957,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -6091,9 +6037,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\");", " },", " );", "}", @@ -6499,6 +6445,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -6883,9 +6840,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\");", " },", " );", "}", @@ -6976,6 +6933,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -7045,9 +7013,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\");", " },", " );", "}", @@ -7405,6 +7373,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -7595,12 +7574,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"processing\" 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 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -7682,6 +7661,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -7904,7 +7894,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -8115,12 +8107,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"processing\" 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 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -8202,6 +8194,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -8356,6 +8359,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 +8564,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": { @@ -8739,6 +8764,17 @@ { "name": "Payments - Retrieve-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 +9097,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 +9241,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 +9385,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" @@ -9464,7 +9530,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -9580,7 +9648,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -9916,6 +9986,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" @@ -9925,7 +10005,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -10042,7 +10124,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 +10233,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" @@ -10151,7 +10252,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -10395,7 +10498,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -10503,6 +10608,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 +10747,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 +11117,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -11251,7 +11368,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -12435,6 +12554,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -12625,12 +12755,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"processing\" 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 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -12895,12 +13025,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"processing\" 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 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -13143,12 +13273,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"processing\" 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 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -13230,6 +13360,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -13384,6 +13525,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": { @@ -13437,13 +13589,13 @@ " );", "}", "", - "// Response body should have value \"Refund amount exceeds the payment amount\" for \"message\"", + "// Response body should have value \"The refund amount exceeds the amount captured\" for \"message\"", "if (jsonData?.error?.message) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", " function () {", " pm.expect(jsonData.error.message).to.eql(", - " \"Refund amount exceeds the payment amount\",", + " \"The refund amount exceeds the amount captured\",", " );", " },", " );", @@ -13614,6 +13766,17 @@ { "name": "Payments - Retrieve", "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 +13896,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": { @@ -13853,11 +14027,12 @@ ] }, "info": { - "_postman_id": "0da8c3be-4466-413e-9e72-81b21533423e", + "_postman_id": "9ab8f157-6b4b-430a-9ca8-34931682f988", "name": "Checkout Collection", "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": "25737662" + "_exporter_id": "25180378", + "_collection_link": "https://galactic-desert-365744.postman.co/workspace/postman-tests-for-all-connector~f274d40a-132c-47e3-8240-6793392ee4d1/collection/25180378-9ab8f157-6b4b-430a-9ca8-34931682f988?action=share&creator=25180378&source=collection_link" }, "variable": [ { diff --git a/postman/collection-json/hyperswitch.postman_collection.json b/postman/collection-json/hyperswitch.postman_collection.json index ab710ca4316a..0acf2ee2b3fc 100644 --- a/postman/collection-json/hyperswitch.postman_collection.json +++ b/postman/collection-json/hyperswitch.postman_collection.json @@ -1915,7 +1915,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"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\":\"giropay\",\"payment_experience\":null,\"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\":\"sofort\",\"payment_experience\":null,\"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\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"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\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"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\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"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\":\"giropay\",\"payment_experience\":null,\"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\":\"sofort\",\"payment_experience\":null,\"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\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"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\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.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", @@ -4798,7 +4798,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"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\":\"giropay\",\"payment_experience\":null,\"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\":\"sofort\",\"payment_experience\":null,\"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\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"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\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"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\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"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\":\"giropay\",\"payment_experience\":null,\"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\":\"sofort\",\"payment_experience\":null,\"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\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"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\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.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", 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 d9deae47f9af..a6ee545a9497 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -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\",\"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},\"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\"}}}" + "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/prophetpay.postman_collection.json b/postman/collection-json/prophetpay.postman_collection.json new file mode 100644 index 000000000000..bd946301f55a --- /dev/null +++ b/postman/collection-json/prophetpay.postman_collection.json @@ -0,0 +1,1418 @@ +{ + "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", + "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" + } + } + ], + "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\",\"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\":\"prophetpay\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"business_country\":\"US\",\"business_label\":\"default\",\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"card_redirect\",\"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}]}],\"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\":10000,\"currency\":\"USD\",\"confirm\":true,\"amount_to_capture\":10000,\"business_country\":\"US\",\"customer_id\":\"not_a_rick_roll\",\"return_url\":\"https://www.google.com\",\"payment_method\":\"card_redirect\",\"payment_method_type\":\"card_redirect\",\"payment_method_data\":{\"card_redirect\":{\"card_redirect\":{}}},\"routing\":{\"type\":\"single\",\"data\":\"prophetpay\"}}" + }, + "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 \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - 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": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":true,\"amount_to_capture\":8000,\"business_country\":\"US\",\"customer_id\":\"not_a_rick_roll\",\"return_url\":\"https://www.google.com\",\"payment_method\":\"card_redirect\",\"payment_method_type\":\"card_redirect\",\"payment_method_data\":{\"card_redirect\":{\"card_redirect\":{}}},\"routing\":{\"type\":\"single\",\"data\":\"prophetpay\"}}" + }, + "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 \"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": "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_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"amount_to_capture\":8000,\"business_country\":\"US\",\"customer_id\":\"not_a_rick_roll\",\"return_url\":\"https://www.google.com\",\"routing\":{\"type\":\"single\",\"data\":\"prophetpay\"}}" + }, + "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\");", + " },", + " );", + "}", + "" + ], + "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_redirect\",\"payment_method_type\":\"card_redirect\",\"payment_method_data\":{\"card_redirect\":{\"card_redirect\":{}}},\"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 \"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": [] + } + ] + } + ] + } + ] + } + ], + "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": "a553df38-fa33-4522-b029-1cd32821730e", + "name": "Prophetpay Postman Collection", + "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": "24206034" + }, + "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": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "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" + } + ] +} diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 5d308dd0fe53..847a1323a2c6 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -423,7 +423,7 @@ } ], "url": { - "raw": "{{baseUrl}}/accounts/list", + "raw": "{{baseUrl}}/accounts/list?organization_id={{organization_id}}", "host": [ "{{baseUrl}}" ], @@ -434,8 +434,7 @@ "query": [ { "key": "organization_id", - "value": "{{organization_id}}", - "disabled": false + "value": "{{organization_id}}" } ], "variable": [ @@ -2080,7 +2079,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}_invalid_values\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"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\":\"giropay\",\"payment_experience\":null,\"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\":\"sofort\",\"payment_experience\":null,\"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\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"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\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"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\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}_invalid_values\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"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\":\"giropay\",\"payment_experience\":null,\"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\":\"sofort\",\"payment_experience\":null,\"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\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"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\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.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", @@ -2350,7 +2349,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"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\":\"giropay\",\"payment_experience\":null,\"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\":\"sofort\",\"payment_experience\":null,\"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\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"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\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"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\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"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\":\"giropay\",\"payment_experience\":null,\"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\":\"sofort\",\"payment_experience\":null,\"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\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"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\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.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/:connector_id", @@ -3540,7 +3539,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", @@ -5665,7 +5664,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"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\":\"giropay\",\"payment_experience\":null,\"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\":\"sofort\",\"payment_experience\":null,\"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\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"Visa\",\"Mastercard\"]}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"Visa\",\"Mastercard\"]}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"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\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"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\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"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\":\"giropay\",\"payment_experience\":null,\"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\":\"sofort\",\"payment_experience\":null,\"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\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"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\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.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", @@ -6055,71 +6054,96 @@ "name": "Happy Cases", "item": [ { - "name": "Scenario24-Add card flow", + "name": "Scenario28-Confirm a payment with requires_customer_action status", "item": [ { - "name": "Payments - Create", + "name": "Payments - Create with confirm true", "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);", - "} 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);", + " 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 {{customer_id}}, as jsonData.customer_id 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\"", + "// 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'\", function() {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " })};" + " pm.test(", + " \"[POST]::/payments - 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;", + " },", + ");", + "" ], "type": "text/javascript" } @@ -6144,7 +6168,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\":\"stripesavecard_{{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\":\"4111111111111111\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"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\":\"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\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"business_country\":\"US\",\"business_label\":\"default\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6160,166 +6184,207 @@ "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(\"[GET]::/payments/:id - Status code is 400\", function () {", + " pm.response.to.be.error;", "});", "", "// 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]::/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){}", + "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 appropriatae error message", + "if (jsonData?.message) {", + " pm.test(", + " \"Content check if appropriate error message is present\",", + " function () {", + " pm.expect(jsonData.message).to.eql(\"You cannot confirm this payment because it has status requires_customer_action\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } } ], "request": { - "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": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"Joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "raw": "{{baseUrl}}/payments/:id/confirm", "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 - } + "payments", + ":id", + "confirm" ], "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": "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": "Scenario1-Create payment with confirm true", + "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 \"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" } @@ -6344,7 +6409,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\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"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,\"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", @@ -6360,29 +6425,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();", "});", "", @@ -6431,26 +6493,24 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", + "// 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" @@ -6458,55 +6518,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\":\"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": [ { @@ -6516,40 +6548,45 @@ } ] }, - "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": "Scenario2-Create payment with confirm false", + "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(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// Validate 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);", @@ -6588,78 +6625,25 @@ " \"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": "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": "Refunds - Create Copy", - "event": [ { - "listen": "test", + "listen": "prerequest", "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.\",", - " );", - "}", "" ], "type": "text/javascript" @@ -6685,355 +6669,392 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"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", + "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\",", - " );", - "});", - "", - "// Set response object as internal variable", + "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 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.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", " );", "}", "" ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { - "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": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"Joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, "url": { - "raw": "{{baseUrl}}/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": "Scenario25-Don't Pass CVV for save card flow and verifysuccess 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.');", - "};" + "// 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" } } ], "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\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"stripesavecard_{{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\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, "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": "Scenario2a-Create payment with confirm false card holder name null", + "item": [ { - "name": "List payment methods for a Customer", + "name": "Payments - Create", "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": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card 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 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\");", + " 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" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" ], "type": "text/javascript" } @@ -7058,7 +7079,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\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -7074,7 +7095,7 @@ "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Payments - Confirm", "event": [ { "listen": "test", @@ -7145,21 +7166,25 @@ " );", "}", "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(\"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " })};" + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" ], "type": "text/javascript" } @@ -7204,7 +7229,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":null,\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -7227,139 +7252,152 @@ "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": "Scenario26-Save card payment with manual capture", - "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);", - "} 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.');", - "};", - "", - "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);", + " 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 {{customer_id}}, as jsonData.customer_id 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 - 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": "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\":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\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, "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": "Scenario2b-Create payment with confirm false card holder name empty", + "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();", "});", "", @@ -7408,47 +7446,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/: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(\"stripe\");", - "});", - "", - "// 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 - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -7456,210 +7459,167 @@ ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "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\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?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": "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 - 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) {}", "", - "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 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_token}}, as jsonData.customer_payment_methods[0].payment_token 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 client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", "}" ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { - "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 - }, + "auth": { + "type": "apikey", + "apikey": [ { - "key": "maximum_amount", - "value": "10000000", - "disabled": true + "key": "value", + "value": "{{publishable_key}}", + "type": "string" }, { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true + "key": "key", + "value": "api-key", + "type": "string" }, { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" + "key": "in", + "value": "header", + "type": "string" } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card 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.');", - "};", - "", - "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": [ { @@ -7678,45 +7638,51 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"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\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "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();", "});", "", @@ -7765,82 +7731,42 @@ " );", "}", "", - "// Response body should have value \"requires_capture\" 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_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", - "}", - "" + "}" ], "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\":\"7373\"}" - }, "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": [ { @@ -7850,34 +7776,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": "Scenario3-Create payment without PMD", + "item": [ { - "name": "Payments - Capture", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - 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/capture - 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/capture - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7926,47 +7854,132 @@ " );", "}", "", - "// 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/:id/capture - 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\");", " },", " );", "}", + "" + ], + "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\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "// 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();", "});", "", - "// 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);", - " },", + "// 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.\",", " );", "}", "", - "// 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);", - " },", + "// 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_capturable\"", - "if (jsonData?.amount_capturable) {", + "// 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 'amount_capturable' matches 'amount - 540'\",", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -7974,9 +7987,38 @@ ], "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": [ { @@ -7995,17 +8037,17 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "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/capture", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "capture" + "confirm" ], "variable": [ { @@ -8015,12 +8057,12 @@ } ] }, - "description": "To capture the funds for an uncaptured 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", @@ -8088,50 +8130,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\");", " },", " );", "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " 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" @@ -8172,69 +8179,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": "Scenario4-Create payment with Manual capture", + "item": [ { - "name": "Refunds - Create Copy", + "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\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", + "// 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 \"requires_capture\" 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_capture'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - "});", "" ], "type": "text/javascript" @@ -8260,139 +8288,45 @@ "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\"}}" + "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}}/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", - "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": [] - } - ] - }, - { - "name": "Scenario27-Create a failure card payment with confirm true", - "item": [ - { - "name": "Payments - Create", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - 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 - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "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 - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -8444,38 +8378,29 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"card_declined\" for \"error_code\"", - "if (jsonData?.error_code) {", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'card_declined'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.error_code).to.eql(\"card_declined\");", + " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", "", - "// Response body should have value \"message - Your card has insufficient funds., decline_code - insufficient_funds\" for \"error_message\"", - "if (jsonData?.error_message) {", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'message - Your card has insufficient funds., decline_code - insufficient_funds'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.error_message).to.eql(\"message - Your card has insufficient funds., decline_code - insufficient_funds\");", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", @@ -8504,18 +8429,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":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_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "capture" + ], + "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 capture the funds for an uncaptured payment" }, "response": [] }, @@ -8588,41 +8522,12 @@ " );", "}", "", - "// 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'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"card_declined\" for \"error_code\"", - "if (jsonData?.error_code) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'card_declined'\",", - " function () {", - " pm.expect(jsonData.error_code).to.eql(\"card_declined\");", - " },", - " );", - "}", - "", - "// Response body should have value \"message - Your card has insufficient funds., decline_code - insufficient_funds\" for \"error_message\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'message - Your card has insufficient funds., decline_code - insufficient_funds'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.error_message).to.eql(\"message - Your card has insufficient funds., decline_code - insufficient_funds\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -8670,7 +8575,7 @@ ] }, { - "name": "Scenario27-Create payment without customer_id and with billing address and shipping address", + "name": "Scenario4a-Create payment with manual_multiple capture", "item": [ { "name": "Payments - Create", @@ -8741,24 +8646,15 @@ " );", "}", "", - "// Response body should have value \"succeeded\" 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 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", - "", - "// 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" @@ -8784,7 +8680,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,\"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", @@ -8800,26 +8696,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - 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(\"[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/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(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -8868,157 +8767,35 @@ " );", "}", "", - "// 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/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\");", " },", " );", "}", "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with confirm true", - "item": [ - { - "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 \"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 \"succeeded\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.amount_received).to.eql(6000);", " },", " );", "}", - "", - "// 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" @@ -9044,18 +8821,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":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_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "capture" + ], + "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 capture the funds for an uncaptured payment" }, "response": [] }, @@ -9128,24 +8914,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 - 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\");", " },", " );", "}", - "", - "// 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" @@ -9190,7 +8967,7 @@ ] }, { - "name": "Scenario2-Create payment with confirm false", + "name": "Scenario5-Void the payment", "item": [ { "name": "Payments - Create", @@ -9261,12 +9038,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" 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_confirmation'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", @@ -9295,7 +9072,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\":\"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\"}}" + "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", @@ -9311,20 +9088,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\",", @@ -9333,7 +9110,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();", "});", "", @@ -9356,19 +9133,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);", @@ -9382,53 +9146,15 @@ " );", "}", "", - "// 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 \"succeeded\" 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 'succeeded'\",", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", " },", " );", "}", - "", - "// 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" @@ -9436,26 +9162,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": [ { @@ -9474,17 +9180,17 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" + "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": [ { @@ -9494,7 +9200,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": [] }, @@ -9567,32 +9273,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" 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 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// 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'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", " },", " );", "}", @@ -9640,7 +9326,7 @@ ] }, { - "name": "Scenario3-Create payment without PMD", + "name": "Scenario6-Create 3DS payment", "item": [ { "name": "Payments - Create", @@ -9711,15 +9397,24 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", "" ], "type": "text/javascript" @@ -9745,7 +9440,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\":\"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\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"business_country\":\"US\",\"business_label\":\"default\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -9761,29 +9456,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();", "});", "", @@ -9831,12 +9523,13 @@ " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", - "// 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 - 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\");", " },", " );", "}", @@ -9844,67 +9537,30 @@ ], "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", + "method": "GET", "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", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "confirm" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -9914,31 +9570,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": "Scenario7-Create 3DS payment with confrm false", + "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();", "});", "", @@ -9987,12 +9648,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:id - 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\");", " },", " );", "}", @@ -10003,66 +9664,63 @@ } ], "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\":\"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\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/: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": "Scenario4-Create payment with Manual capture", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -10111,156 +9769,33 @@ " );", "}", "", - "// Response body should have value \"requires_capture\" 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_capture'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "" + "", + "// 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;", + " },", + ");", + "" ], "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", + "listen": "prerequest", "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 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// 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" @@ -10268,6 +9803,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -10286,17 +9841,17 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/capture", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "capture" + "confirm" ], "variable": [ { @@ -10306,7 +9861,7 @@ } ] }, - "description": "To capture the funds for an uncaptured 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": [] }, @@ -10379,12 +9934,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 - 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\");", " },", " );", "}", @@ -10432,7 +9987,7 @@ ] }, { - "name": "Scenario5-Void the payment", + "name": "Scenario8-Create a failure card payment with confirm true", "item": [ { "name": "Payments - Create", @@ -10503,12 +10058,41 @@ " );", "}", "", - "// Response body should have value \"requires_capture\" 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_capture'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"card_declined\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'card_declined'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"card_declined\");", + " },", + " );", + "}", + "", + "// Response body should have value \"message - Your card has insufficient funds., decline_code - insufficient_funds\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'message - Your card has insufficient funds., decline_code - insufficient_funds'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"message - Your card has insufficient funds., decline_code - insufficient_funds\");", " },", " );", "}", @@ -10537,7 +10121,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\":\"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\"}}" + "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", @@ -10553,29 +10137,26 @@ "response": [] }, { - "name": "Payments - Cancel", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - 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/cancel - 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/cancel - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -10598,6 +10179,19 @@ " );", "}", "", + "// 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);", @@ -10611,139 +10205,41 @@ " );", "}", "", - "// Response body should have value \"cancelled\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", " },", " );", "}", - "" - ], - "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.\",", - " );", - "}", + "// 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;", + " },", + ");", "", - "// 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 \"card_declined\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'card_declined'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"card_declined\");", + " },", " );", "}", "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"message - Your card has insufficient funds., decline_code - insufficient_funds\" for \"error_message\"", + "if (jsonData?.error_message) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'message - Your card has insufficient funds., decline_code - insufficient_funds'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " pm.expect(jsonData.error_message).to.eql(\"message - Your card has insufficient funds., decline_code - insufficient_funds\");", " },", " );", "}", @@ -10791,7 +10287,7 @@ ] }, { - "name": "Scenario6-Create 3DS payment", + "name": "Scenario9-Refund full payment", "item": [ { "name": "Payments - Create", @@ -10862,24 +10358,15 @@ " );", "}", "", - "// 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 - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", "" ], "type": "text/javascript" @@ -10905,7 +10392,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"business_country\":\"US\",\"business_label\":\"default\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "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\":\"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", @@ -10989,12 +10476,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/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -11038,87 +10525,61 @@ "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": "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);", + "// 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 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", " );", "}", "", - "// Response body should have value \"requires_confirmation\" 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_confirmation'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", @@ -11147,46 +10608,38 @@ "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\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"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": "Payments - Confirm", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - 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/: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(\"[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", @@ -11195,216 +10648,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'\",", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// 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;", - " },", - ");", - "" - ], - "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 \"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);", " },", " );", "}", @@ -11423,36 +10695,30 @@ } ], "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": "Scenario9-Refund full payment", + "name": "Scenario9a-Partial refund", "item": [ { "name": "Payments - Create", @@ -11739,12 +11005,12 @@ " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// Response body should have value \"540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", @@ -11773,7 +11039,7 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "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", @@ -11839,9 +11105,9 @@ "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", @@ -11879,87 +11145,61 @@ "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-Partial refund", - "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.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);", + "// 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,", - " );", - "} 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,", + " \"- 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 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"1000\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.amount).to.eql(1000);", " },", " );", "}", @@ -11988,23 +11228,115 @@ "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\":\"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\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"Customer returned product\",\"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": "Payments - Retrieve", + "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 '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "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": "Payments - Retrieve-copy", "event": [ { "listen": "test", @@ -12081,6 +11413,11 @@ " },", " );", "}", + "", + "// 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" @@ -12121,64 +11458,116 @@ "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": "Scenario10-Create a mandate and recurring payment", + "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.\",", + " );", + "}", + "", + "// 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]::/refunds - 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 \"540\" for \"amount\"", + "// 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 - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " 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" @@ -12204,78 +11593,115 @@ "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\"}}" + "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\"}},\"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\":\"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}}/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 {{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 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", + "// 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 '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\");", " },", " );", "}", + "", + "// 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" @@ -12291,83 +11717,134 @@ } ], "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": "Refunds - Create-copy", + "name": "Recurring 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\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", + "// 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 \"1000\" 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 - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", + " 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" @@ -12393,110 +11870,18 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"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" - ] + "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\":\"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\"}}" }, - "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 '1000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } + "payments" ] }, - "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 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": [] }, @@ -12579,11 +11964,22 @@ " );", "}", "", - "// Response body should have \"refunds\"", - "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", - " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", - "});", - "" + "// 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" } @@ -12627,7 +12023,7 @@ ] }, { - "name": "Scenario11-Create a mandate and recurring payment", + "name": "Scenario11-Refund recurring payment", "item": [ { "name": "Payments - Create", @@ -12758,7 +12154,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\"}},\"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\":\"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\"}}" + "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\"}},\"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\":\"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", @@ -12987,6 +12383,16 @@ " );", "}", "", + "// 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\",", @@ -13035,7 +12441,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\":\"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", @@ -13184,116 +12590,64 @@ "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-Refund recurring payment", - "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.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);", + "// 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 - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/refunds - 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 \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.amount).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 \"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" @@ -13319,115 +12673,78 @@ "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\"}},\"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\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"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": "Payments - Retrieve", + "name": "Refunds - Retrieve Copy", "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 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", " );", "}", "", - "// Response body should have value \"Succeeded\" 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 'succeeded'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.amount).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 \"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" @@ -13443,34 +12760,33 @@ } ], "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": "Scenario12-BNPL-klarna", + "item": [ { - "name": "Recurring Payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", @@ -13538,49 +12854,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_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\");", " },", " );", "}", - "", - "// 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" @@ -13606,7 +12888,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\":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", @@ -13622,26 +12904,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();", "});", "", @@ -13690,31 +12975,53 @@ " );", "}", "", - "// 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\"", + "// Response body should have \"next_action.redirect_to_url\"", "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"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 value \"klarna\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'klarna'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"klarna\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ "" ], "type": "text/javascript" @@ -13722,27 +13029,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": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"klarna_redirect\":{\"issuer_name\":\"stripe\",\"billing_email\":\"arjun.karthik@juspay.in\",\"billing_country\":\"US\"}}},\"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": [ { @@ -13752,64 +13087,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 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", + "// 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\");", " },", " );", "}", @@ -13820,185 +13176,94 @@ } ], "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\":\"Customer returned product\",\"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": "Scenario13-BNPL-afterpay", + "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\",", " );", "});", "", + "// 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 '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 '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": "Scenario12-BNPL-klarna", - "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,", + "// 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(", @@ -14053,7 +13318,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\":7000,\"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\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"country\":\"SE\"}},\"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\":\"SE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -14159,12 +13424,12 @@ " },", ");", "", - "// Response body should have value \"klarna\" for \"payment_method_type\"", + "// Response body should have value \"afterpay_clearpay\" for \"payment_method_type\"", "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'klarna'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'afterpay_clearpay'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"klarna\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"afterpay_clearpay\");", " },", " );", "}", @@ -14232,7 +13497,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"klarna_redirect\":{\"issuer_name\":\"stripe\",\"billing_email\":\"arjun.karthik@juspay.in\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"afterpay_clearpay_redirect\":{\"billing_name\":\"Akshaya\",\"billing_email\":\"example@example.com\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -14378,7 +13643,7 @@ ] }, { - "name": "Scenario13-BNPL-afterpay", + "name": "Scenario14-BNPL-affirm", "item": [ { "name": "Payments - Create", @@ -14483,7 +13748,7 @@ "language": "json" } }, - "raw": "{\"amount\":7000,\"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\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"country\":\"SE\"}},\"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\":\"SE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":7000,\"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\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"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\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -14589,12 +13854,12 @@ " },", ");", "", - "// Response body should have value \"afterpay_clearpay\" for \"payment_method_type\"", + "// Response body should have value \"affirm\" for \"payment_method_type\"", "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'afterpay_clearpay'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'affirm'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"afterpay_clearpay\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"affirm\");", " },", " );", "}", @@ -14662,7 +13927,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"afterpay_clearpay_redirect\":{\"billing_name\":\"Akshaya\",\"billing_email\":\"example@example.com\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"affirm_redirect\":{\"issuer_name\":\"affirm\",\"billing_email\":\"user-us@example.com\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -14808,7 +14073,7 @@ ] }, { - "name": "Scenario14-BNPL-affirm", + "name": "Scenario15-Bank Redirect-Ideal", "item": [ { "name": "Payments - Create", @@ -14913,7 +14178,7 @@ "language": "json" } }, - "raw": "{\"amount\":7000,\"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\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"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\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "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\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -15019,12 +14284,12 @@ " },", ");", "", - "// Response body should have value \"affirm\" for \"payment_method_type\"", + "// Response body should have value \"ideal\" for \"payment_method_type\"", "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'affirm'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ideal'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"affirm\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"ideal\");", " },", " );", "}", @@ -15092,7 +14357,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"affirm_redirect\":{\"issuer_name\":\"affirm\",\"billing_email\":\"user-us@example.com\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -15238,7 +14503,7 @@ ] }, { - "name": "Scenario15-Bank Redirect-Ideal", + "name": "Scenario16-Bank Redirect-sofort", "item": [ { "name": "Payments - Create", @@ -15449,12 +14714,12 @@ " },", ");", "", - "// Response body should have value \"ideal\" for \"payment_method_type\"", + "// 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 'ideal'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"ideal\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", " },", " );", "}", @@ -15522,7 +14787,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_noe_lb_fur_niederosterreich_u_wien\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -15668,7 +14933,7 @@ ] }, { - "name": "Scenario16-Bank Redirect-sofort", + "name": "Scenario17-Bank Redirect-eps", "item": [ { "name": "Payments - Create", @@ -15879,12 +15144,12 @@ " },", ");", "", - "// Response body should have value \"sofort\" for \"payment_method_type\"", + "// 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 'sofort'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", " },", " );", "}", @@ -15952,7 +15217,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_noe_lb_fur_niederosterreich_u_wien\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_oberosterreich_salzburg_steiermark\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -16098,7 +15363,7 @@ ] }, { - "name": "Scenario17-Bank Redirect-eps", + "name": "Scenario18-Bank Redirect-giropay", "item": [ { "name": "Payments - Create", @@ -16309,12 +15574,12 @@ " },", ");", "", - "// Response body should have value \"eps\" for \"payment_method_type\"", + "// 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 'eps'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", " },", " );", "}", @@ -16382,7 +15647,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_oberosterreich_salzburg_steiermark\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -16528,7 +15793,7 @@ ] }, { - "name": "Scenario18-Bank Redirect-giropay", + "name": "Scenario19-Bank Transfer-ach", "item": [ { "name": "Payments - Create", @@ -16633,7 +15898,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\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"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://hs-payments-test.netlify.app/payments\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -16730,21 +15995,32 @@ " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", + "// Response body should have \"next_action.type\"", "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " \"[POST]::/payments - Content check if 'next_action.type' exists\",", " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", + " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", " },", ");", "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", + "// 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 'giropay'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", + " },", + " );", + "}", + "", + "// Response body should have value \"display_bank_transfer_information\" for \"next_action.type\"", + "if (jsonData?.next_action.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'display_bank_transfer_information'\",", + " function () {", + " pm.expect(jsonData.next_action.type).to.eql(", + " \"display_bank_transfer_information\",", + " );", " },", " );", "}", @@ -16812,7 +16088,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_method\":\"bank_transfer\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_transfer\":{\"ach_bank_transfer\":{\"billing_details\":{\"email\":\"johndoe@example.com\"}}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -16958,7 +16234,7 @@ ] }, { - "name": "Scenario19-Bank Transfer-ach", + "name": "Scenario20-Bank Debit-ach", "item": [ { "name": "Payments - Create", @@ -17029,12 +16305,12 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -17042,6 +16318,15 @@ ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { @@ -17063,7 +16348,7 @@ "language": "json" } }, - "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"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://hs-payments-test.netlify.app/payments\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":1800,\"currency\":\"USD\",\"confirm\":true,\"business_label\":\"default\",\"capture_method\":\"automatic\",\"connector\":[\"stripe\"],\"customer_id\":\"klarna\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"authentication_type\":\"three_ds\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"return_url\":\"https://google.com\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"setup_future_usage\":\"off_session\",\"business_country\":\"US\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"online\",\"accepted_at\":\"2022-09-10T10:11:12Z\",\"online\":{\"ip_address\":\"123.32.25.123\",\"user_agent\":\"Mozilla/5.0 (Linux; Android 12; SM-S906N Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36\"}},\"mandate_type\":{\"single_use\":{\"amount\":6540,\"currency\":\"USD\"}}},\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"billing_details\":{\"name\":\"John Doe\",\"email\":\"johndoe@example.com\"},\"account_number\":\"000123456789\",\"routing_number\":\"110000000\"}}},\"metadata\":{\"order_details\":{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":1800,\"account_name\":\"transaction_processing\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -17079,29 +16364,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();", "});", "", @@ -17153,88 +16435,2515 @@ "// 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'\",", + " \"[POST]::/payments:id - 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.type\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.type' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// 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 \"display_bank_transfer_information\" for \"next_action.type\"", - "if (jsonData?.next_action.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'display_bank_transfer_information'\",", - " function () {", - " pm.expect(jsonData.next_action.type).to.eql(", - " \"display_bank_transfer_information\",", - " );", + "" + ], + "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": "Scenario21-Wallet-Wechatpay", + "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\":800,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"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\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"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 \"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 value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" + "// Response body should have \"next_action.type\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.type' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// 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 'we_chat_pay'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"we_chat_pay\");", + " },", + " );", + "}", + "", + "// Response body should have value \"qr_code_information\" for \"next_action.type\"", + "if (jsonData?.next_action.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'qr_code_information'\",", + " function () {", + " pm.expect(jsonData.next_action.type).to.eql(\"qr_code_information\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "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\":\"wallet\",\"payment_method_type\":\"we_chat_pay\",\"payment_method_data\":{\"wallet\":{\"we_chat_pay_qr\":{}}},\"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 \"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": "Scenario22- Update address and List Payment method", + "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.');", + "};", + "", + "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", + "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" + } + } + ], + "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\"}},\"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": "List Payment Methods for a Merchant", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Parse the response body as JSON", + "var responseBody = pm.response.json();", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"card\";", + " });", + "});", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"pay_later\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'pay_later'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"pay_later\";", + " });", + "});", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"wallet\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'wallet'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"wallet\";", + " });", + "});", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_debit\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_debit'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"bank_debit\";", + " });", + "});", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_transfer\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_transfer'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"bank_transfer\";", + " });", + "});" + ], + "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": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + "payment_methods" + ], + "query": [ + { + "key": "client_secret", + "value": "{{client_secret}}" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular merchant id." + }, + "response": [] + }, + { + "name": "Payments - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/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 - 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 - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "", + "// Parse the JSON response", + "var jsonData = pm.response.json();", + "", + "// Check if the 'currency' is equal to \"EUR\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'currency' matches 'EUR' \", function () {", + " pm.expect(jsonData.currency).to.eql(\"EUR\");", + "});", + "", + "// Extract the \"country\" field from the JSON data", + "var country = jsonData.billing.address.country;", + "", + "// Check if the country is \"NL\"", + "pm.test(\"[POST]::/payments/:id -Content Check if billing 'Country' matches NL (Netherlands)\", function () {", + " pm.expect(country).to.equal(\"NL\");", + "});", + "", + "var country1 = jsonData.shipping.address.country;", + "", + "// Check if the country is \"NL\"", + "pm.test(\"[POST]::/payments/:id -Content Check if shipping 'Country' matches NL (Netherlands)\", function () {", + " pm.expect(country1).to.equal(\"NL\");", + "});", + "" + ], + "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": "{\"currency\":\"EUR\",\"shipping\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}},\"billing\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] + }, + "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " + }, + "response": [] + }, + { + "name": "List Payment Methods for a Merchant-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "", + "// Parse the response body as JSON", + "var responseBody = pm.response.json();", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"card\";", + " });", + "});", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"ideal\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Content Check if payment_method matches 'ideal'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"ideal\";", + " });", + "});", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_redirect\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_redirect'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"bank_redirect\";", + " });", + "});" + ], + "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": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + "payment_methods" + ], + "query": [ + { + "key": "client_secret", + "value": "{{client_secret}}" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular merchant id." + }, + "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\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "//// 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_customer_action'\", function() {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + "})};", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"Example\",\"email\":\"guest@example.com\"},\"bank_name\":\"ing\"}}},\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"125.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"multi_use\":{\"amount\":7000,\"currency\":\"USD\",\"start_date\":\"2023-04-21T00:00:00Z\",\"end_date\":\"2023-05-21T00:00:00Z\",\"metadata\":{\"frequency\":\"13\"}}}},\"setup_future_usage\":\"off_session\",\"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\":\"128.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{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": "Scenario23- Update Amount", + "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.');", + "};", + "", + "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", + "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" + } + } + ], + "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\"}},\"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 - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/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 - 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 - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "", + "// Parse the JSON response", + "var jsonData = pm.response.json();", + "", + "// Check if the 'amount' is equal to \"1000\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + "});", + "", + "", + "" + ], + "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}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] + }, + "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " + }, + "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\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "//// 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\");", + "})};", + "", + "", + "// Check if the 'amount' is equal to \"1000\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + "});", + "", + "//// Response body should have value \"amount_received\" for \"1000\"", + "if (jsonData?.amount_received) {", + "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", + " pm.expect(jsonData.amount_received).to.eql(1000);", + "})};", + "" + ], + "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" + }, + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + }, + { + "key": "publishable_key", + "value": "", + "type": "text", + "disabled": true + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"setup_future_usage\":\"off_session\",\"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\":\"128.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{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 \"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\");", + "})};", + "", + "", + "// Check if the 'amount' is equal to \"1000\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + "});", + "", + "//// Response body should have value \"amount_received\" for \"1000\"", + "if (jsonData?.amount_received) {", + "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", + " pm.expect(jsonData.amount_received).to.eql(1000);", + "})};", + "" + ], + "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": "Scenario24-Add card flow", + "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.');", + "};", + "", + "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/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " })};" + ], + "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\":\"stripesavecard_{{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\":\"4111111111111111\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"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\":\"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\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"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.');", + "}" + ], + "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": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card 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.');", + "};", + "", + "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\":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\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"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": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "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\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" + }, + "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\",", + " );", + "});", + "", + "// 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": "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.\",", + " );", + "}", + "" + ], + "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\":600,\"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", + "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.\",", + " );", + "}", + "" + ], + "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": "Scenario25-Don't Pass CVV for save card flow and verifysuccess 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.');", + "};", + "", + "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\":\"stripesavecard_{{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\"},\"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": "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": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card 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.');", + "};", + "", + "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": [ { @@ -17253,51 +18962,45 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"bank_transfer\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_transfer\":{\"ach_bank_transfer\":{\"billing_details\":{\"email\":\"johndoe@example.com\"}}}},\"client_secret\":\"{{client_secret}}\"}" + "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\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Save card 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();", "});", "", @@ -17346,43 +19049,76 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", " },", " );", "}", - "" + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(\"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " })};" ], "type": "text/javascript" } } ], "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_token\":\"{{payment_token}}\"}" + }, "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": [ { @@ -17392,14 +19128,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" + "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": "Scenario19-Bank Debit-ach", + "name": "Scenario26-Save card payment with manual capture", "item": [ { "name": "Payments - Create", @@ -17408,87 +19144,66 @@ "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_customer_action\" for \"status\"", + "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 'requires_customer_action'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" + "}" ], "type": "text/javascript" } @@ -17513,7 +19228,7 @@ "language": "json" } }, - "raw": "{\"amount\":1800,\"currency\":\"USD\",\"confirm\":true,\"business_label\":\"default\",\"capture_method\":\"automatic\",\"connector\":[\"stripe\"],\"customer_id\":\"klarna\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"authentication_type\":\"three_ds\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"return_url\":\"https://google.com\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"setup_future_usage\":\"off_session\",\"business_country\":\"US\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"online\",\"accepted_at\":\"2022-09-10T10:11:12Z\",\"online\":{\"ip_address\":\"123.32.25.123\",\"user_agent\":\"Mozilla/5.0 (Linux; Android 12; SM-S906N Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36\"}},\"mandate_type\":{\"single_use\":{\"amount\":6540,\"currency\":\"USD\"}}},\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"billing_details\":{\"name\":\"John Doe\",\"email\":\"johndoe@example.com\"},\"account_number\":\"000123456789\",\"routing_number\":\"110000000\"}}},\"metadata\":{\"order_details\":{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":1800,\"account_name\":\"transaction_processing\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "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\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -17584,25 +19299,60 @@ " );", "}", "", - "// 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.\",", + "// 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\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "});", + "", + "// 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 \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -17646,91 +19396,168 @@ "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": "Scenario22-Wallet-Wechatpay", - "item": [ + }, { - "name": "Payments - Create", + "name": "List payment methods for a Customer", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// 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": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// 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" } @@ -17755,7 +19582,7 @@ "language": "json" } }, - "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"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\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"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\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -17771,7 +19598,7 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", @@ -17842,44 +19669,17 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"requires_capture\" 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.type\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.type' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// 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 'we_chat_pay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"we_chat_pay\");", - " },", - " );", - "}", - "", - "// Response body should have value \"qr_code_information\" for \"next_action.type\"", - "if (jsonData?.next_action.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'qr_code_information'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_capture'\",", " function () {", - " pm.expect(jsonData.next_action.type).to.eql(\"qr_code_information\");", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", "", + "", "// Response body should have value \"stripe\" for \"connector\"", "if (jsonData?.connector) {", " pm.test(", @@ -17893,15 +19693,6 @@ ], "type": "text/javascript" } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } } ], "request": { @@ -17943,7 +19734,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"wallet\",\"payment_method_type\":\"we_chat_pay\",\"payment_method_data\":{\"wallet\":{\"we_chat_pay_qr\":{}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -17968,26 +19759,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - 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(\"[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/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(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -18036,12 +19830,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/capture - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "});", + "", + "// 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 '6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", " },", " );", "}", @@ -18052,27 +19881,35 @@ } ], "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_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } + ":id", + "capture" ], "variable": [ { @@ -18082,288 +19919,225 @@ } ] }, - "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 capture the funds for an uncaptured payment" }, "response": [] - } - ] - }, - { - "name": "Scenario22- Update address and List Payment method", - "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);", + " 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.\",", + " );", + "}", "", - "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", - "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\"", + "// 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'\", 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\"}},\"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": "List Payment Methods for a Merchant", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Parse the response body as JSON", - "var responseBody = pm.response.json();", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"card\";", - " });", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", "});", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"pay_later\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'pay_later'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"pay_later\";", - " });", - "});", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"wallet\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'wallet'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"wallet\";", - " });", - "});", + "// 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);", + " },", + " );", + "}", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_debit\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_debit'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"bank_debit\";", - " });", - "});", + "// 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 '6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_transfer\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_transfer'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"bank_transfer\";", - " });", - "});" + "// 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" } } ], "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": "GET", "header": [ { "key": "Accept", "value": "application/json" - }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true } ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "account", - "payment_methods" + "payments", + ":id" ], "query": [ { - "key": "client_secret", - "value": "{{client_secret}}" + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To filter and list the applicable payment methods for a particular merchant 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": "Payments - Update", + "name": "Refunds - Create Copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments/:id - 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/: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 - 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) {}", "", - "// Parse the JSON response", - "var jsonData = pm.response.json();", - "", - "// Check if the 'currency' is equal to \"EUR\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'currency' matches 'EUR' \", function () {", - " pm.expect(jsonData.currency).to.eql(\"EUR\");", - "});", - "", - "// Extract the \"country\" field from the JSON data", - "var country = jsonData.billing.address.country;", + "// 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.\",", + " );", + "}", "", - "// Check if the country is \"NL\"", - "pm.test(\"[POST]::/payments/:id -Content Check if billing 'Country' matches NL (Netherlands)\", function () {", - " pm.expect(country).to.equal(\"NL\");", - "});", + "// 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\");", + " },", + " );", + "}", "", - "var country1 = jsonData.shipping.address.country;", + "// 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);", + " },", + " );", + "}", "", - "// Check if the country is \"NL\"", - "pm.test(\"[POST]::/payments/:id -Content Check if shipping 'Country' matches NL (Netherlands)\", function () {", - " pm.expect(country1).to.equal(\"NL\");", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", "});", "" ], @@ -18390,172 +20164,205 @@ "language": "json" } }, - "raw": "{\"currency\":\"EUR\",\"shipping\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}},\"billing\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}}}" + "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}}/payments/:id", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}" - } + "refunds" ] }, - "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "List Payment Methods for a Merchant-copy", + "name": "Refunds - Retrieve Copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:merchant_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/:merchant_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) {}", "", - "// Parse the response body as JSON", - "var responseBody = pm.response.json();", - "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"card\";", - " });", - "});", + "// 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.\",", + " );", + "}", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"ideal\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Content Check if payment_method matches 'ideal'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"ideal\";", - " });", - "});", + "// 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\");", + " },", + " );", + "}", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_redirect\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_redirect'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"bank_redirect\";", - " });", - "});" + "// 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": { - "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": "GET", "header": [ { "key": "Accept", "value": "application/json" - }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true } ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "account", - "payment_methods" + "refunds", + ":id" ], - "query": [ + "variable": [ { - "key": "client_secret", - "value": "{{client_secret}}" + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "To filter and list the applicable payment methods for a particular merchant 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": "Scenario27-Create payment without customer_id and with billing address and shipping address", + "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 - 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) {}", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments/:id/confirm - 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.\",", + " );", + "}", "", - "//// Response body should have value \"requires_customer_action\" 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 \"succeeded\" for \"status\"", "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\", function() {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - "})};", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", "" ], "type": "text/javascript" @@ -18563,26 +20370,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": [ { @@ -18601,26 +20388,18 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"Example\",\"email\":\"guest@example.com\"},\"bank_name\":\"ing\"}}},\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"125.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"multi_use\":{\"amount\":7000,\"currency\":\"USD\",\"start_date\":\"2023-04-21T00:00:00Z\",\"end_date\":\"2023-05-21T00:00:00Z\",\"metadata\":{\"frequency\":\"13\"}}}},\"setup_future_usage\":\"off_session\",\"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\":\"128.0.0.1\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"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/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{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": [] }, @@ -18631,55 +20410,87 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[GET]::/payments/:id - 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(\"[GET]::/payments/:id - 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(\"[GET]::/payments/:id - 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 \"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 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\");", - "})};" + "// 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" } @@ -18721,9 +20532,14 @@ "response": [] } ] - }, + } + ] + }, + { + "name": "Variation Cases", + "item": [ { - "name": "Scenario23- Update Amount", + "name": "Scenario10-Refund exceeds amount captured", "item": [ { "name": "Payments - Create", @@ -18732,62 +20548,77 @@ "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.\",", + " );", + "}", "", - "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", - "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\"", + "// 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'\", function() {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - "})};", + " 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" @@ -18813,7 +20644,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\":\"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\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+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", @@ -18829,127 +20660,106 @@ "response": [] }, { - "name": "Payments - Update", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// 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 - 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 - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "", - "// Parse the JSON response", - "var jsonData = pm.response.json();", - "", - "// Check if the 'amount' is equal to \"1000\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - "});", - "", - "", - "" - ], - "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}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}" - } - ] - }, - "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " - }, - "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;", - "});", + "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 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/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){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments/:id/confirm - 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.\",", + " );", + "}", "", - "//// 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\");", - "})};", + "// 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\");", + " },", + " );", + "}", "", - "// Check if the 'amount' is equal to \"1000\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - "});", + "// 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 \"amount_received\" for \"1000\"", + "// Response body should have value \"6000\" for \"amount_received\"", "if (jsonData?.amount_received) {", - "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", - " pm.expect(jsonData.amount_received).to.eql(1000);", - "})};", + " 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" @@ -18957,26 +20767,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": [ { @@ -18986,18 +20776,6 @@ { "key": "Accept", "value": "application/json" - }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - }, - { - "key": "publishable_key", - "value": "", - "type": "text", - "disabled": true } ], "body": { @@ -19007,26 +20785,27 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"setup_future_usage\":\"off_session\",\"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\":\"128.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": [ { "key": "id", - "value": "{{payment_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" + "description": "To capture the funds for an uncaptured payment" }, "response": [] }, @@ -19037,67 +20816,77 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[GET]::/payments/:id - 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(\"[GET]::/payments/:id - 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(\"[GET]::/payments/:id - 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 \"succeeded\" for \"status\"", + "// 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\");", - "})};", - "", - "", - "// Check if the 'amount' is equal to \"1000\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - "});", - "", - "//// Response body should have value \"amount_received\" for \"1000\"", - "if (jsonData?.amount_received) {", - "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", - " pm.expect(jsonData.amount_received).to.eql(1000);", - "})};", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -19138,14 +20927,114 @@ "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", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// 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 \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"The refund amount exceeds the amount captured\" for \"error message\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"The refund amount exceeds the amount captured\");", + " },", + " );", + "}", + "" + ], + "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\":\"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": "Variation Cases", - "item": [ + }, { "name": "Scenario1-Create payment with Invalid card details", "item": [ diff --git a/postman/collection-json/trustpay.postman_collection.json b/postman/collection-json/trustpay.postman_collection.json index b88fa9e110c9..cacc015851d8 100644 --- a/postman/collection-json/trustpay.postman_collection.json +++ b/postman/collection-json/trustpay.postman_collection.json @@ -811,7 +811,7 @@ "name": "Happy Cases", "item": [ { - "name": "Scenario1-Create payment with confirm true", + "name": "Scenario2a-Create payment with confirm false card holder name empty", "item": [ { "name": "Payments - Create", @@ -882,12 +882,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\");", " },", " );", "}", @@ -916,7 +916,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\":\"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\":\"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\"}},\"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": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -931,6 +931,155 @@ }, "response": [] }, + { + "name": "Payments - Confirm", + "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/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "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\":\"\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, { "name": "Payments - Retrieve", "event": [ @@ -1000,16 +1149,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\");", " },", " );", - "}", - "" + "}" ], "type": "text/javascript" } @@ -1053,7 +1201,7 @@ ] }, { - "name": "Scenario2-Create payment with confirm false", + "name": "Scenario2b-Create payment with confirm false card holder name null", "item": [ { "name": "Payments - Create", @@ -1124,12 +1272,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[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\");", " },", " );", "}", @@ -1158,7 +1306,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\"}},\"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\",\"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", @@ -1299,7 +1447,7 @@ "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": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":null,\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -2303,6 +2451,15 @@ ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { @@ -2344,7 +2501,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"return_url\":\"https://integ.hyperswitch.io/home\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5200000000000015\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"\",\"card_cvc\":\"737\",\"card_issuer\":\"\",\"card_network\":\"Visa\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\",\"accept_header\":\"text\\\\/html,application\\\\/xhtml+xml,application\\\\/xml;q=0.9,image\\\\/webp,image\\\\/apng,*\\\\/*;q=0.8\",\"language\":\"en-GB\",\"color_depth\":30,\"ip_address\":\"65.1.52.138\",\"screen_height\":1117,\"screen_width\":1728,\"time_zone\":-330,\"java_enabled\":true,\"java_script_enabled\":true}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"return_url\":\"https://integ.hyperswitch.io/home\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5200000000000015\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"John Doe\",\"card_cvc\":\"737\",\"card_issuer\":\"\",\"card_network\":\"Visa\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\",\"accept_header\":\"text\\\\/html,application\\\\/xhtml+xml,application\\\\/xml;q=0.9,image\\\\/webp,image\\\\/apng,*\\\\/*;q=0.8\",\"language\":\"en-GB\",\"color_depth\":30,\"ip_address\":\"65.1.52.138\",\"screen_height\":1117,\"screen_width\":1728,\"time_zone\":-330,\"java_enabled\":true,\"java_script_enabled\":true}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -2922,7 +3079,7 @@ ] }, { - "name": "Scenario8-Bank Redirect-Ideal", + "name": "Scenario7-Bank Redirect-Ideal", "item": [ { "name": "Payments - Create", @@ -3344,7 +3501,7 @@ ] }, { - "name": "Scenario11-Bank Redirect-giropay", + "name": "Scenario8-Bank Redirect-giropay", "item": [ { "name": "Payments - Create", @@ -3764,25 +3921,20 @@ "response": [] } ] - } - ] - }, - { - "name": "Variation Cases", - "item": [ + }, { - "name": "Scenario1-Create payment with Invalid card details", + "name": "Scenario1-Create payment with confirm true", "item": [ { - "name": "Payments - Create(Invalid card number)", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", + "// 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", @@ -3842,17 +3994,12 @@ " );", "}", "", - "// Response body should have \"error\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector_error'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.error.type).to.eql(\"connector_error\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3881,7 +4028,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\":\"12345\",\"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\":\"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\"}},\"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": "{\"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\":\"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\":\"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\"}},\"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", @@ -3897,26 +4044,26 @@ "response": [] }, { - "name": "Payments - Create(Invalid Exp month)", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", + "// 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.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();", "});", "", @@ -3965,17 +4112,12 @@ " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3986,41 +4128,799 @@ } ], "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\":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\":\"5100000000000511\",\"card_exp_month\":\"19\",\"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\":\"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\"}},\"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", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "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" + "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_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"Joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[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\");", + " },", + " );", + "}", + "" + ], + "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": "Variation Cases", + "item": [ + { + "name": "Scenario5-Refund for unsuccessful 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_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\":\"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\"}},\"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", + "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 \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "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": "Payments - Create(Invalid", + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// 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 \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "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": "Scenario1-Create payment with Invalid card details", + "item": [ + { + "name": "Payments - Create(Invalid card number)", "event": [ { "listen": "test", @@ -4088,7 +4988,7 @@ " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", + "// Response body should have \"error\"", "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", "});", @@ -4096,9 +4996,9 @@ "// Response body should have value \"connector error\" for \"error type\"", "if (jsonData?.error?.type) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector_error'\",", " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " pm.expect(jsonData.error.type).to.eql(\"connector_error\");", " },", " );", "}", @@ -4127,7 +5027,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\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"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\"}},\"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": "{\"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\":\"12345\",\"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\":\"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\"}},\"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", @@ -4143,7 +5043,7 @@ "response": [] }, { - "name": "Payments - Create(invalid CVV)", + "name": "Payments - Create(Invalid Exp month)", "event": [ { "listen": "test", @@ -4211,7 +5111,7 @@ " );", "}", "", - "// Response body should have \"error\"", + "// Response body should have \"next_action.redirect_to_url\"", "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", "});", @@ -4250,7 +5150,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\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"1234\"}},\"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\"}},\"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": "{\"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\":\"5100000000000511\",\"card_exp_month\":\"19\",\"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\":\"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\"}},\"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", @@ -4264,22 +5164,17 @@ "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-Confirming the payment without PMD", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Create(Invalid", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", "});", "", "// Validate if response header has matching content-type", @@ -4339,12 +5234,17 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", " },", " );", "}", @@ -4373,7 +5273,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"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\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"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\"}},\"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", @@ -4389,7 +5289,7 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Create(invalid CVV)", "event": [ { "listen": "test", @@ -4400,18 +5300,15 @@ " pm.response.to.be.error;", "});", "", - "// 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 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/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4461,17 +5358,14 @@ "}", "", "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", "", "// Response body should have value \"connector error\" for \"error type\"", "if (jsonData?.error?.type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", " function () {", " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", " },", @@ -4484,26 +5378,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": [ { @@ -4522,34 +5396,25 @@ "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\":\"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\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"1234\"}},\"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\"}},\"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/: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": "Scenario3-Capture the succeeded payment", + "name": "Scenario2-Confirming the payment without PMD", "item": [ { "name": "Payments - Create", @@ -4620,12 +5485,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\");", " },", " );", "}", @@ -4654,7 +5519,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\":\"5100000000000511\",\"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\"}},\"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": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4670,20 +5535,20 @@ "response": [] }, { - "name": "Payments - Capture", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", " pm.response.to.be.error;", "});", "", "// Validate if response header has matching content-type", "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", " function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", @@ -4692,7 +5557,7 @@ ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4765,6 +5630,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -4783,17 +5668,17 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/capture", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "capture" + "confirm" ], "variable": [ { @@ -4803,14 +5688,14 @@ } ] }, - "description": "To capture the funds for an uncaptured 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": "Scenario4-Refund exceeds amount", + "name": "Scenario3-Capture the succeeded payment", "item": [ { "name": "Payments - Create", @@ -4931,26 +5816,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Capture", "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 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "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(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4999,94 +5887,6 @@ " );", "}", "", - "// 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" - } - } - ], - "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", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// 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 \"error\"", "pm.test(", " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", @@ -5129,25 +5929,34 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "description": "To capture the funds for an uncaptured payment" }, "response": [] } ] }, { - "name": "Scenario8-Refund for unsuccessful payment", + "name": "Scenario4-Refund exceeds amount", "item": [ { "name": "Payments - Create", @@ -5218,12 +6027,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" 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_confirmation'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -5252,7 +6061,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\"}},\"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": "{\"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\":\"5100000000000511\",\"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\"}},\"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", @@ -5336,12 +6145,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" 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_confirmation'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -5432,7 +6241,7 @@ " },", ");", "", - "// Response body should have value \"invalid_request\" for \"error type\"", + "// Response body should have value \"connector error\" for \"error type\"", "if (jsonData?.error?.type) {", " pm.test(", " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", @@ -5466,7 +6275,7 @@ "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\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/refunds", diff --git a/postman/collection-json/wise.postman_collection.json b/postman/collection-json/wise.postman_collection.json new file mode 100644 index 000000000000..410f066ff6fb --- /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\":\"sdk-test-app.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/postman/portman-config.json b/postman/portman-config.json index 599767fd6f17..7f94c31a5212 100644 --- a/postman/portman-config.json +++ b/postman/portman-config.json @@ -1583,7 +1583,7 @@ "responseBodyTests": [ { "key": "message", - "value": "Refund amount exceeds the payment amount" + "value": "The refund amount exceeds the amount captured" } ] } diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 9fdc57bf3c81..c1f59cb36359 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen airwallex applepay authorizedotnet bambora bankofamerica bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu powertranz prophetpay rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bankofamerica bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu placetopay powertranz prophetpay rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp @@ -25,7 +25,7 @@ function find_prev_connector() { eval "$2='aci'" } -payment_gateway=$1; +payment_gateway=$(echo $1 | tr '[:upper:]' '[:lower:]') base_url=$2; payment_gateway_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${payment_gateway:0:1})${payment_gateway:1}" src="crates/router/src" @@ -45,24 +45,28 @@ 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='' -find_prev_connector $1 previous_connector +find_prev_connector $payment_gateway previous_connector previous_connector_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${previous_connector:0:1})${previous_connector:1}" 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 diff --git a/scripts/decrypt_connector_auth.sh b/scripts/decrypt_connector_auth.sh deleted file mode 100755 index dc445f0afa6b..000000000000 --- a/scripts/decrypt_connector_auth.sh +++ /dev/null @@ -1,10 +0,0 @@ -#! /usr/bin/env bash - -mkdir -p $HOME/target/test - - -# Decrypt the file -# --batch to prevent interactive command -# --yes to assume "yes" for questions -gpg --quiet --batch --yes --decrypt --passphrase="$CONNECTOR_AUTH_PASSPHRASE" \ ---output $HOME/target/test/connector_auth.toml .github/secrets/connector_auth.toml.gpg \ No newline at end of file