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/workflows/release-nightly-version-reusable.yml b/.github/workflows/release-nightly-version-reusable.yml index deb8c44cc3c3..accd8c12a913 100644 --- a/.github/workflows/release-nightly-version-reusable.yml +++ b/.github/workflows/release-nightly-version-reusable.yml @@ -3,11 +3,8 @@ name: Create a nightly tag on: workflow_call: secrets: - app_id: - description: App ID for the GitHub app - required: true - app_private_key: - description: Private key for the GitHub app + token: + description: GitHub token for authenticating with GitHub required: true outputs: tag: @@ -31,23 +28,17 @@ jobs: 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.app_id }} - private-key: ${{ secrets.app_private_key }} - - 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}}'" + 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 @@ -139,62 +130,22 @@ jobs: }' CHANGELOG.md rm release-notes.md - # We make use of GitHub API calls to commit and tag the changelog instead of the simpler - # `git commit`, `git tag` and `git push` commands to have signed commits and tags - - name: Commit generated changelog and create tag + - 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 - env: - GH_TOKEN: ${{ steps.generate_app_token.outputs.token }} run: | - HEAD_COMMIT="$(git rev-parse 'HEAD^{commit}')" - - # Create a tree based on the HEAD commit of the current branch and updated changelog file - TREE_SHA="$( - gh api \ - --method POST \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - '/repos/{owner}/{repo}/git/trees' \ - --raw-field base_tree="${HEAD_COMMIT}" \ - --raw-field 'tree[][path]=CHANGELOG.md' \ - --raw-field 'tree[][mode]=100644' \ - --raw-field 'tree[][type]=blob' \ - --field 'tree[][content]=@CHANGELOG.md' \ - --jq '.sha' - )" - - # Create a commit to point to the above created tree - NEW_COMMIT_SHA="$( - gh api \ - --method POST \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - '/repos/{owner}/{repo}/git/commits' \ - --raw-field "message=chore(version): ${NEXT_TAG}" \ - --raw-field "parents[]=${HEAD_COMMIT}" \ - --raw-field "tree=${TREE_SHA}" \ - --jq '.sha' - )" - - # Update the current branch to point to the above created commit - # We disable forced update so that the workflow will fail if the branch has been updated since the workflow started - # (for example, new commits were pushed to the branch after the workflow execution started). - gh api \ - --method PATCH \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/{owner}/{repo}/git/refs/heads/${ALLOWED_BRANCH_NAME}" \ - --raw-field "sha=${NEW_COMMIT_SHA}" \ - --field 'force=false' - - # Create a lightweight tag to point to the above created commit - 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=${NEW_COMMIT_SHA}" + 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 diff --git a/.github/workflows/release-nightly-version.yml b/.github/workflows/release-nightly-version.yml index 36a843469d0c..13e844e7c5d7 100644 --- a/.github/workflows/release-nightly-version.yml +++ b/.github/workflows/release-nightly-version.yml @@ -27,23 +27,17 @@ jobs: 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 + 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}}'" + 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 @@ -80,66 +74,21 @@ jobs: echo "Postman collection files have no modifications" fi - - name: Commit updated Postman collections if modified + - name: Set git configuration shell: bash - env: - GH_TOKEN: ${{ steps.generate_app_token.outputs.token }} if: ${{ env.POSTMAN_COLLECTION_FILES_UPDATED == 'true' }} run: | - # Obtain current HEAD commit SHA and use that as base tree SHA for creating a new tree - HEAD_COMMIT="$(git rev-parse 'HEAD^{commit}')" - UPDATED_TREE_SHA="${HEAD_COMMIT}" - - # Obtain the flags to be passed to the GitHub CLI. - # Each line contains the flags to be used corresponding to the file. - lines="$( - git ls-files \ - --format '--raw-field tree[][path]=%(path) --raw-field tree[][mode]=%(objectmode) --raw-field tree[][type]=%(objecttype) --field tree[][content]=@%(path)' \ - postman/collection-json - )" - - # Create a tree based on the HEAD commit of the current branch, using the contents of the updated Postman collections directory - while IFS= read -r line; do - # Split each line by space to obtain the flags passed to the GitHub CLI as an array - IFS=' ' read -ra flags <<< "${line}" - - # Create a tree by updating each collection JSON file. - # The SHA of the created tree is used as the base tree SHA for updating the next collection file. - UPDATED_TREE_SHA="$( - gh api \ - --method POST \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - '/repos/{owner}/{repo}/git/trees' \ - --raw-field base_tree="${UPDATED_TREE_SHA}" \ - "${flags[@]}" \ - --jq '.sha' - )" - done <<< "${lines}" - - # Create a commit to point to the tree with all updated collections - NEW_COMMIT_SHA="$( - gh api \ - --method POST \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - '/repos/{owner}/{repo}/git/commits' \ - --raw-field "message=chore(postman): update Postman collection files" \ - --raw-field "parents[]=${HEAD_COMMIT}" \ - --raw-field "tree=${UPDATED_TREE_SHA}" \ - --jq '.sha' - )" - - # Update the current branch to point to the above created commit. - # We disable forced update so that the workflow will fail if the branch has been updated since the workflow started - # (for example, new commits were pushed to the branch after the workflow execution started). - gh api \ - --method PATCH \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/{owner}/{repo}/git/refs/heads/${ALLOWED_BRANCH_NAME}" \ - --raw-field "sha=${NEW_COMMIT_SHA}" \ - --field 'force=false' + 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 @@ -147,5 +96,4 @@ jobs: needs: - update-postman-collections secrets: - app_id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} - app_private_key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + token: ${{ secrets.AUTO_RELEASE_PAT }} 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/CHANGELOG.md b/CHANGELOG.md index 32504f7f0974..739b8cd2c667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,78 @@ All notable changes to HyperSwitch will be documented here. - - - +## 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 diff --git a/config/config.example.toml b/config/config.example.toml index 9749a01a8ae5..e20f9c1b65d2 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -139,12 +139,6 @@ 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 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 @@ -345,7 +339,7 @@ sts_role_session_name = "" # An identifier for the assumed role session, used to #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" } @@ -357,6 +351,7 @@ stripe = { payment_method = "bank_transfer" } nuvei = { payment_method = "card" } shift4 = { payment_method = "card" } bluesnap = { payment_method = "card" } +cybersource = {payment_method = "card"} nmi = {payment_method = "card"} [dummy_connector] @@ -473,7 +468,7 @@ apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Cer apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm [payment_link] -sdk_url = "http://localhost:9090/dist/HyperLoader.js" +sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" [payment_method_auth] redis_expiry = 900 diff --git a/config/development.toml b/config/development.toml index 65ec470d19db..5732d5f0d1de 100644 --- a/config/development.toml +++ b/config/development.toml @@ -80,12 +80,6 @@ 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 = "" @@ -421,7 +415,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"} @@ -434,6 +428,7 @@ stripe = {payment_method = "bank_transfer"} nuvei = {payment_method = "card"} shift4 = {payment_method = "card"} bluesnap = {payment_method = "card"} +cybersource = {payment_method = "card"} nmi = {payment_method = "card"} [connector_customer] @@ -494,7 +489,7 @@ 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 diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 59aaba2e5098..c6934a64671f 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -58,12 +58,6 @@ mock_locker = true basilisk_host = "" [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 = "" @@ -235,7 +229,7 @@ consumer_group = "SCHEDULER_GROUP" #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"} @@ -247,6 +241,7 @@ stripe = {payment_method = "bank_transfer"} nuvei = {payment_method = "card"} shift4 = {payment_method = "card"} bluesnap = {payment_method = "card"} +cybersource = {payment_method = "card"} nmi = {payment_method = "card"} [dummy_connector] diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs index 964486c93649..b8fd5e6a35d0 100644 --- a/crates/analytics/src/clickhouse.rs +++ b/crates/analytics/src/clickhouse.rs @@ -21,6 +21,7 @@ use crate::{ filters::ApiEventFilter, metrics::{latency::LatencyAvg, ApiEventMetricRow}, }, + outgoing_webhook_event::events::OutgoingWebhookLogsResult, sdk_events::events::SdkEventsResult, types::TableEngine, }; @@ -120,6 +121,7 @@ impl AnalyticsDataSource for ClickhouseClient { } AnalyticsCollection::SdkEvents => TableEngine::BasicTree, AnalyticsCollection::ApiEvents => TableEngine::BasicTree, + AnalyticsCollection::OutgoingWebhookEvent => TableEngine::BasicTree, } } } @@ -145,6 +147,10 @@ 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::outgoing_webhook_event::events::OutgoingWebhookLogsFilterAnalytics + for ClickhouseClient +{ +} #[derive(Debug, serde::Serialize)] struct CkhQuery { @@ -302,6 +308,18 @@ impl TryInto for serde_json::Value { } } +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse OutgoingWebhookLogsResult in clickhouse results", + )) + } +} + impl ToSql for PrimitiveDateTime { fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { let format = @@ -326,6 +344,7 @@ impl ToSql for AnalyticsCollection { Self::SdkEvents => Ok("sdk_events_dist".to_string()), Self::ApiEvents => Ok("api_audit_log".to_string()), Self::PaymentIntent => Ok("payment_intents_dist".to_string()), + Self::OutgoingWebhookEvent => Ok("outgoing_webhook_events_audit".to_string()), } } } diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 24da77f84f2b..8529807a1a16 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -7,6 +7,7 @@ mod query; pub mod refunds; pub mod api_event; +pub mod outgoing_webhook_event; pub mod sdk_events; mod sqlx; mod types; 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/sqlx.rs b/crates/analytics/src/sqlx.rs index cdd2647e4e71..e32b85a53672 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -429,6 +429,8 @@ impl ToSql for AnalyticsCollection { 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::OutgoingWebhookEvent => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("OutgoingWebhookEvents table is not implemented for Sqlx"))?, } } } diff --git a/crates/analytics/src/types.rs b/crates/analytics/src/types.rs index 8b1bdbd1ab92..8da4655e255b 100644 --- a/crates/analytics/src/types.rs +++ b/crates/analytics/src/types.rs @@ -26,6 +26,7 @@ pub enum AnalyticsCollection { SdkEvents, ApiEvents, PaymentIntent, + OutgoingWebhookEvent, } #[allow(dead_code)] diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index c588bb87189f..134beacd226f 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1175,6 +1175,9 @@ pub struct PaymentLinkConfigRequest { /// 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)] @@ -1185,4 +1188,6 @@ pub struct PaymentLinkConfig { 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 0263427b0fde..e0d3fa671b60 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -12,6 +12,7 @@ use self::{ pub use crate::payments::TimeRange; pub mod api_event; +pub mod outgoing_webhook_event; pub mod payments; pub mod refunds; pub mod sdk_events; 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/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/events.rs b/crates/api_models/src/events.rs index 457d3fde05b7..6d9bd5db3429 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -17,7 +17,9 @@ use common_utils::{ use crate::{ admin::*, - analytics::{api_event::*, sdk_events::*, *}, + analytics::{ + api_event::*, outgoing_webhook_event::OutgoingWebhookLogsRequest, sdk_events::*, *, + }, api_keys::*, cards_info::*, disputes::*, @@ -89,7 +91,8 @@ impl_misc_api_event_type!( ApiLogsRequest, GetApiEventMetricRequest, SdkEventsRequest, - ReportRequest + ReportRequest, + OutgoingWebhookLogsRequest ); #[cfg(feature = "stripe")] diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 459443747e36..dc1f6eb65375 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -3,6 +3,7 @@ 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; diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 85b0adefca5f..a907fff60193 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -13,9 +13,7 @@ use utoipa::{schema, ToSchema}; #[cfg(feature = "payouts")] use crate::payouts; use crate::{ - admin, - customers::CustomerId, - enums as api_enums, + admin, enums as api_enums, payments::{self, BankCodeResponse}, }; @@ -459,8 +457,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, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 76d62605a43a..cac94a07326a 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1129,6 +1129,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>, @@ -1665,6 +1666,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>, @@ -1707,7 +1709,7 @@ pub enum VoucherData { #[serde(rename_all = "snake_case")] pub enum PaymentMethodDataResponse { #[serde(rename = "card")] - Card(CardResponse), + Card(Box), BankTransfer, Wallet, PayLater, @@ -2037,7 +2039,7 @@ pub struct PaymentsResponse { #[schema(example = 100)] pub amount: i64, - /// The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount, + /// 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, @@ -2274,6 +2276,9 @@ pub struct PaymentsResponse { /// List of incremental authorizations happened to the payment pub incremental_authorizations: Option>, + + /// Payment Fingerprint + pub fingerprint: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -2528,6 +2533,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, @@ -2538,7 +2544,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, @@ -3358,6 +3364,13 @@ 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: String, @@ -3374,6 +3387,22 @@ pub struct PaymentLinkDetails { pub max_items_visible_after_collapse: i8, 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 intent_status: api_enums::IntentStatus, + pub payment_link_status: PaymentLinkStatus, + pub error_code: Option, + pub error_message: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -3451,7 +3480,8 @@ pub struct OrderDetailsWithStringAmount { pub product_img_link: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] pub enum PaymentLinkStatus { Active, Expired, diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index e89de9c58934..1a0668023f02 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -127,7 +127,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)] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 07909a35782e..f5af31c8e7f6 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -86,6 +86,7 @@ pub struct InviteUserRequest { #[derive(Debug, serde::Serialize)] pub struct InviteUserResponse { pub is_email_sent: bool, + pub password: Option>, } #[derive(Debug, serde::Deserialize, serde::Serialize)] diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index bc8e75f6d479..7b3564732bf9 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 diff --git a/crates/cards/src/validate.rs b/crates/cards/src/validate.rs index ca47c73c7c2c..0bb07b83dc68 100644 --- a/crates/cards/src/validate.rs +++ b/crates/cards/src/validate.rs @@ -24,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() @@ -35,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 { diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 0c4b9720cab8..949cc2e0034d 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -6,12 +6,13 @@ use utoipa::ToSchema; 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, }; } @@ -275,6 +276,27 @@ pub enum AuthorizationStatus { 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, @@ -921,10 +943,14 @@ impl Currency { #[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, diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 169cb972c066..cd24e430b76d 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -48,8 +48,8 @@ pub const PROPHETPAY_REDIRECT_URL: &str = "https://ccm-thirdparty.cps.golf/hp/to /// Variable which store the card token for Prophetpay pub const PROPHETPAY_TOKEN: &str = "cctoken"; -/// Payment intent fulfillment default timeout (in seconds) -pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; +/// 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/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index b24de92de101..dfa0a9ec9232 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -639,8 +639,6 @@ merchant_id_classic="MerchantId Classic" password_evoucher="Password Evoucher" username_evoucher="Username Evoucher" merchant_id_evoucher="MerchantId Evoucher" - - [cashtocode.connector_webhook_details] merchant_secret="Source verification key" @@ -1246,8 +1244,9 @@ label="apple" payment_method_type = "apple_pay" [[nmi.wallet]] payment_method_type = "google_pay" -[nmi.connector_auth.HeaderKey] +[nmi.connector_auth.BodyKey] api_key="API Key" +key1="Public Key" [nmi.connector_webhook_details] merchant_secret="Source verification key" @@ -2084,6 +2083,8 @@ api_key = "Username" api_secret = "Password" key1 = "Client ID" key2 = "Client Secret" +[volt.connector_webhook_details] +merchant_secret="Source verification key" [worldline] [[worldline.credit]] diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 38a41b40f7a7..e837314f6106 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -359,6 +359,9 @@ 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]] @@ -514,7 +517,8 @@ merchant_id_classic="MerchantId Classic" 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]] @@ -1059,10 +1063,9 @@ label="apple" [nmi] [[nmi.bank_redirect]] payment_method_type = "ideal" -[nmi.connector_auth.SignatureKey] -api_key="Client ID" -key1="Airline ID" -api_secret="Client Secret" +[nmi.connector_auth.BodyKey] +api_key="API Key" +key1="Public Key" [nmi.connector_webhook_details] merchant_secret="Source verification key" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index c41ad7793e8e..47de5cd5d5ff 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -639,6 +639,8 @@ merchant_id_classic="MerchantId Classic" 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]] @@ -1242,8 +1244,9 @@ label="apple" payment_method_type = "apple_pay" [[nmi.wallet]] payment_method_type = "google_pay" -[nmi.connector_auth.HeaderKey] +[nmi.connector_auth.BodyKey] api_key="API Key" +key1="Public Key" [nmi.connector_webhook_details] merchant_secret="Source verification key" @@ -2080,6 +2083,8 @@ api_key = "Username" api_secret = "Password" key1 = "Client ID" key2 = "Client Secret" +[volt.connector_webhook_details] +merchant_secret="Source verification key" [worldline] [[worldline.credit]] diff --git a/crates/data_models/src/errors.rs b/crates/data_models/src/errors.rs index 9616a3a944ca..bed1ab9ccbf5 100644 --- a/crates/data_models/src/errors.rs +++ b/crates/data_models/src/errors.rs @@ -24,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/payments.rs b/crates/data_models/src/payments.rs index cc6b03f89a5b..713003d666b2 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -53,5 +53,6 @@ pub struct PaymentIntent { 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_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 80671ec7f61d..7470b5f85028 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -110,6 +110,7 @@ pub struct PaymentIntentNew { pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub fingerprint_id: Option, pub session_expiry: Option, } @@ -163,6 +164,7 @@ pub enum PaymentIntentUpdate { metadata: Option, payment_confirm_source: Option, updated_by: String, + fingerprint_id: Option, session_expiry: Option, }, PaymentAttemptAndAttemptCountUpdate { @@ -228,6 +230,7 @@ pub struct PaymentIntentUpdateInternal { pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub fingerprint_id: Option, pub session_expiry: Option, } @@ -252,6 +255,7 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, } => Self { amount: Some(amount), @@ -272,6 +276,7 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, ..Default::default() }, 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/enums.rs b/crates/diesel_models/src/enums.rs index 792e8ffc8bb3..a06937c99a6d 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -2,9 +2,9 @@ pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, - DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, - DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType, - DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, + DbBlocklistDataKind as BlocklistDataKind, DbCaptureMethod as CaptureMethod, + DbCaptureStatus as CaptureStatus, DbConnectorStatus as ConnectorStatus, + DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDashboardMetadata as DashboardMetadata, DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index fa32fb84a15d..82b1e29ee838 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -1,11 +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/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 6b546f90787e..31bc0c06c51d 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -56,6 +56,7 @@ pub struct PaymentIntent { pub incremental_authorization_allowed: Option, pub authorization_count: Option, pub session_expiry: Option, + pub fingerprint_id: Option, } #[derive( @@ -107,6 +108,7 @@ pub struct PaymentIntentNew { 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)] @@ -160,6 +162,7 @@ pub enum PaymentIntentUpdate { payment_confirm_source: Option, updated_by: String, session_expiry: Option, + fingerprint_id: Option, }, PaymentAttemptAndAttemptCountUpdate { active_attempt_id: String, @@ -226,6 +229,7 @@ pub struct PaymentIntentUpdateInternal { pub incremental_authorization_allowed: Option, pub authorization_count: Option, pub session_expiry: Option, + pub fingerprint_id: Option, } impl PaymentIntentUpdate { @@ -259,6 +263,7 @@ impl PaymentIntentUpdate { incremental_authorization_allowed, authorization_count, session_expiry, + fingerprint_id, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -288,10 +293,12 @@ impl PaymentIntentUpdate { 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), - session_expiry, + fingerprint_id: fingerprint_id.or(source.fingerprint_id), + session_expiry: session_expiry.or(source.session_expiry), ..source } } @@ -319,6 +326,7 @@ impl From for PaymentIntentUpdateInternal { payment_confirm_source, updated_by, session_expiry, + fingerprint_id, } => Self { amount: Some(amount), currency: Some(currency), @@ -339,6 +347,7 @@ impl From for PaymentIntentUpdateInternal { payment_confirm_source, updated_by, session_expiry, + fingerprint_id, ..Default::default() }, PaymentIntentUpdate::MetadataUpdate { diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index 3a3dee47a854..3a0a008b76bd 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -1,11 +1,14 @@ 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; 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/schema.rs b/crates/diesel_models/src/schema.rs index b29a362e3b02..131d2b182661 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::*; @@ -709,6 +753,8 @@ diesel::table! { incremental_authorization_allowed -> Nullable, authorization_count -> Nullable, session_expiry -> Nullable, + #[max_length = 64] + fingerprint_id -> Nullable, } } @@ -1016,6 +1062,9 @@ diesel::table! { diesel::allow_tables_to_appear_in_same_query!( address, api_keys, + blocklist, + blocklist_fingerprint, + blocklist_lookup, business_profile, captures, cards_info, diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index eb7fbc7ddbc9..8ecac3620919 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -14,9 +14,8 @@ s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config", "olap"] frm = [] -basilisk = ["kms"] stripe = ["dep:serde_qs"] -release = ["kms", "stripe", "basilisk", "s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen"] +release = ["kms", "stripe", "s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index f31e908e0dc3..c62de5bd29ab 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -4,7 +4,7 @@ pub mod routes { use actix_web::{web, Responder, Scope}; use analytics::{ api_event::api_events_core, errors::AnalyticsError, lambda_utils::invoke_lambda, - sdk_events::sdk_events_core, + outgoing_webhook_event::outgoing_webhook_events_core, sdk_events::sdk_events_core, }; use api_models::analytics::{ GenerateReportRequest, GetApiEventFiltersRequest, GetApiEventMetricRequest, @@ -71,6 +71,10 @@ pub mod routes { ) .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("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)), @@ -314,6 +318,30 @@ pub mod routes { .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, diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 5963110c6324..63205ea68ca6 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -520,6 +520,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(), }, 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/kms.rs b/crates/router/src/configs/kms.rs index bf6ee44d28be..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), diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index db59d7f29148..3d93c2f188b7 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -287,6 +287,15 @@ pub struct PaymentMethodTokenFilter { 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)] @@ -499,12 +508,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, diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index 1e0856a9ccc4..aeb3dafcfa21 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -205,7 +205,7 @@ impl ConnectorCommon for Bankofamerica { }; match response { transformers::BankOfAmericaErrorResponse::StandardError(response) => { - let (code, message) = match response.error_information { + let (code, connector_reason) = match response.error_information { Some(ref error_info) => (error_info.reason.clone(), error_info.message.clone()), None => ( response @@ -218,13 +218,13 @@ impl ConnectorCommon for Bankofamerica { .map_or(error_message.to_string(), |message| message), ), }; - let connector_reason = match response.details { + let message = match response.details { Some(details) => details .iter() .map(|det| format!("{} : {}", det.field, det.reason)) .collect::>() .join(", "), - None => message.clone(), + None => connector_reason.clone(), }; Ok(ErrorResponse { diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 71a44b5a6e67..6abe1b634df6 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -343,6 +343,30 @@ 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: String, +} + impl TryFrom<( &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, @@ -658,10 +682,12 @@ pub struct BankOfAmericaClientReferenceResponse { id: String, status: BankofamericaPaymentStatus, client_reference_information: ClientReferenceInformation, + processor_information: Option, + risk_information: Option, error_information: Option, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BankOfAmericaErrorInformationResponse { id: String, @@ -674,6 +700,55 @@ pub struct BankOfAmericaErrorInformation { 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, @@ -684,6 +759,7 @@ fn get_error_response_if_failure( if utils::is_payment_failure(status) { Some(types::ErrorResponse::from(( &info_response.error_information, + &info_response.risk_information, http_code, info_response.id.clone(), ))) @@ -706,7 +782,10 @@ fn get_payment_response( resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), redirection_data: None, mandate_reference: None, - connector_metadata: 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 @@ -752,26 +831,13 @@ impl ..item.data }) } - BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { - response: { - let error_reason = &error_response.error_information.reason; - - Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message, - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id), - }) - }, - status: enums::AttemptStatus::Failure, - ..item.data - }), + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from(( + &error_response.clone(), + item, + Some(enums::AttemptStatus::Failure), + ))) + } } } } @@ -806,24 +872,9 @@ impl ..item.data }) } - BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { - response: { - let error_reason = &error_response.error_information.reason; - Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message, - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id), - }) - }, - ..item.data - }), + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item, None))) + } } } } @@ -858,24 +909,9 @@ impl ..item.data }) } - BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { - response: { - let error_reason = &error_response.error_information.reason; - Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message, - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id), - }) - }, - ..item.data - }), + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item, None))) + } } } } @@ -927,10 +963,12 @@ impl app_response.application_information.status, item.data.request.is_auto_capture()?, )); + 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(), ))), @@ -988,6 +1026,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>> @@ -997,6 +1037,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 { @@ -1007,6 +1051,7 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> client_reference_information: ClientReferenceInformation { code: Some(value.router_data.connector_request_reference_id.clone()), }, + merchant_defined_information, }) } } @@ -1016,6 +1061,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)] @@ -1032,6 +1080,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()), @@ -1054,6 +1106,7 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCancelRouterData>> field_name: "Cancellation Reason", })?, }, + merchant_defined_information, }) } } @@ -1198,8 +1251,8 @@ pub struct BankOfAmericaAuthenticationErrorResponse { #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum BankOfAmericaErrorResponse { - StandardError(BankOfAmericaStandardErrorResponse), AuthenticationError(BankOfAmericaAuthenticationErrorResponse), + StandardError(BankOfAmericaStandardErrorResponse), } #[derive(Debug, Deserialize, Clone)] @@ -1220,29 +1273,53 @@ pub struct AuthenticationErrorInformation { pub rmsg: String, } -impl From<(&Option, u16, String)> for types::ErrorResponse { +impl + From<( + &Option, + &Option, + u16, + String, + )> for types::ErrorResponse +{ fn from( - (error_data, status_code, transaction_id): ( + (error_data, risk_information, status_code, transaction_id): ( &Option, + &Option, u16, String, ), ) -> Self { - let error_message = error_data + let avs_message = risk_information .clone() - .and_then(|error_details| error_details.message); + .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_reason + code: error_message .clone() .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason + message: error_message .clone() .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_message.clone(), + 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/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index edcad00c9830..e54d8320d0ff 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -1043,11 +1043,19 @@ impl api::IncomingWebhook for Bluesnap { | bluesnap::BluesnapWebhookEvents::Charge | bluesnap::BluesnapWebhookEvents::Chargeback | bluesnap::BluesnapWebhookEvents::ChargebackStatusChanged => { - 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( diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 33503102e4b5..69159c10c8af 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -12,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}, @@ -124,7 +125,7 @@ impl ConnectorCommon for Cybersource { }; match response { transformers::CybersourceErrorResponse::StandardError(response) => { - let (code, message) = match response.error_information { + let (code, connector_reason) = match response.error_information { Some(ref error_info) => (error_info.reason.clone(), error_info.message.clone()), None => ( response @@ -137,13 +138,13 @@ impl ConnectorCommon for Cybersource { .map_or(error_message.to_string(), |message| message), ), }; - let connector_reason = match response.details { + let message = match response.details { Some(details) => details .iter() .map(|det| format!("{} : {}", det.field, det.reason)) .collect::>() .join(", "), - None => message.clone(), + None => connector_reason.clone(), }; Ok(types::ErrorResponse { @@ -286,6 +287,8 @@ 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 @@ -472,6 +475,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 { @@ -672,13 +782,20 @@ 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", + api::ConnectorCommon::base_url(self, connectors) + )) + } else { + Ok(format!( + "{}pts/v2/payments/", + api::ConnectorCommon::base_url(self, connectors) + )) + } } fn get_request_body( @@ -692,9 +809,15 @@ impl ConnectorIntegration CustomResult { + if data.is_three_ds() && data.request.is_card() { + 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) + } +} + +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) + )) + } + 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") diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index e46833d2ecde..e83b23603e9b 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -1,6 +1,7 @@ use api_models::payments; use base64::Engine; -use common_utils::pii; +use common_utils::{ext_traits::ValueExt, pii}; +use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -8,10 +9,12 @@ use serde_json::Value; use crate::{ connector::utils::{ self, AddressDetailsData, ApplePayDecrypt, CardData, PaymentsAuthorizeRequestData, + PaymentsCompleteAuthorizeRequestData, PaymentsPreProcessingData, PaymentsSetupMandateRequestData, PaymentsSyncRequestData, RouterData, }, consts, core::errors, + services, types::{ self, api::{self, enums as api_enums}, @@ -200,7 +203,7 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { action_list, action_token_types, authorization_options, - commerce_indicator: CybersourceCommerceIndicator::Internet, + commerce_indicator: String::from("internet"), payment_solution: solution.map(String::from), }; Ok(Self { @@ -220,6 +223,8 @@ pub struct CybersourcePaymentsRequest { 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>, } @@ -229,12 +234,22 @@ pub struct ProcessingInformation { action_list: Option>, action_token_types: Option>, authorization_options: Option, - commerce_indicator: CybersourceCommerceIndicator, + 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 { @@ -282,12 +297,6 @@ pub enum CybersourcePaymentInitiatorTypes { Customer, } -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum CybersourceCommerceIndicator { - Internet, -} - #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CaptureOptions { @@ -450,6 +459,16 @@ impl From<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> } } +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 From<( &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, @@ -489,7 +508,56 @@ impl action_token_types, authorization_options, capture_options: None, - commerce_indicator: CybersourceCommerceIndicator::Internet, + 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: 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")), } } } @@ -516,6 +584,28 @@ impl } } +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, @@ -602,6 +692,84 @@ impl payment_information, order_information, client_reference_information, + consumer_authentication_information: None, + merchant_defined_information, + }) + } +} + +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + payments::Card, + )> for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + 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, + }, + }); + 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: "Merchant connector account 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, }) } @@ -647,6 +815,7 @@ impl payment_information, order_information, client_reference_information, + consumer_authentication_information: None, merchant_defined_information, }) } @@ -689,6 +858,7 @@ impl payment_information, order_information, client_reference_information, + consumer_authentication_information: None, merchant_defined_information, }) } @@ -747,6 +917,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> order_information, client_reference_information, merchant_defined_information, + consumer_authentication_information: None, }) } } @@ -810,6 +981,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> order_information, client_reference_information, merchant_defined_information, + consumer_authentication_information: None, }) } payments::PaymentMethodData::CardRedirect(_) @@ -832,11 +1004,72 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> } } +#[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)] @@ -853,6 +1086,10 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> 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 { @@ -863,7 +1100,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> action_token_types: None, authorization_options: None, capture: None, - commerce_indicator: CybersourceCommerceIndicator::Internet, + commerce_indicator: String::from("internet"), payment_solution: None, }, order_information: OrderInformationWithBill { @@ -873,6 +1110,10 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> }, bill_to: None, }, + client_reference_information: ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }, + merchant_defined_information, }) } } @@ -898,7 +1139,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRout reason: "5".to_owned(), }), }), - commerce_indicator: CybersourceCommerceIndicator::Internet, + commerce_indicator: String::from("internet"), capture: None, capture_options: None, payment_solution: None, @@ -918,6 +1159,9 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRout 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)] @@ -932,6 +1176,10 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCancelRouterData>> for Cyber 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()), @@ -954,6 +1202,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCancelRouterData>> for Cyber field_name: "Cancellation Reason", })?, }, + merchant_defined_information, }) } } @@ -1086,6 +1335,8 @@ pub struct CybersourceClientReferenceResponse { id: String, status: CybersourcePaymentStatus, client_reference_information: ClientReferenceInformation, + processor_information: Option, + risk_information: Option, token_information: Option, error_information: Option, } @@ -1097,6 +1348,29 @@ pub struct CybersourceErrorInformationResponse { 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 { @@ -1117,6 +1391,30 @@ 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: String, +} + +#[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 { @@ -1133,10 +1431,11 @@ impl From<( &CybersourceErrorInformationResponse, types::ResponseRouterData, + Option, )> for types::RouterData { fn from( - (error_response, item): ( + (error_response, item, transaction_status): ( &CybersourceErrorInformationResponse, types::ResponseRouterData< F, @@ -1144,25 +1443,35 @@ impl T, types::PaymentsResponseData, >, + Option, ), ) -> Self { - Self { - response: { - let error_reason = &error_response.error_information.reason; - Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message.clone(), - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id.clone()), - }) + 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 }, - ..item.data } } } @@ -1177,6 +1486,7 @@ fn get_error_response_if_failure( if utils::is_payment_failure(status) { Some(types::ErrorResponse::from(( &info_response.error_information, + &info_response.risk_information, http_code, info_response.id.clone(), ))) @@ -1210,7 +1520,10 @@ fn get_payment_response( resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), redirection_data: None, mandate_reference, - connector_metadata: 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 @@ -1257,25 +1570,11 @@ impl ..item.data }) } - CybersourcePaymentsResponse::ErrorInformation(error_response) => { - let error_reason = &error_response.error_information.reason; - Ok(Self { - response: Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message, - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id.clone()), - }), - status: enums::AttemptStatus::Failure, - ..item.data - }) - } + CybersourcePaymentsResponse::ErrorInformation(ref error_response) => Ok(Self::from(( + &error_response.clone(), + item, + Some(enums::AttemptStatus::Failure), + ))), } } } @@ -1284,23 +1583,509 @@ impl TryFrom< types::ResponseRouterData< F, - CybersourcePaymentsResponse, - types::PaymentsCaptureData, + CybersourceAuthSetupResponse, + types::PaymentsAuthorizeData, types::PaymentsResponseData, >, - > for types::RouterData + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, - CybersourcePaymentsResponse, - types::PaymentsCaptureData, + CybersourceAuthSetupResponse, + types::PaymentsAuthorizeData, types::PaymentsResponseData, >, ) -> Result { match item.response { - CybersourcePaymentsResponse::ClientReferenceInformation(info_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)); @@ -1311,7 +2096,7 @@ impl }) } CybersourcePaymentsResponse::ErrorInformation(ref error_response) => { - Ok(Self::from((&error_response.clone(), item))) + Ok(Self::from((&error_response.clone(), item, None))) } } } @@ -1348,7 +2133,7 @@ impl }) } CybersourcePaymentsResponse::ErrorInformation(ref error_response) => { - Ok(Self::from((&error_response.clone(), item))) + Ok(Self::from((&error_response.clone(), item, None))) } } } @@ -1417,25 +2202,29 @@ impl ..item.data }) } - CybersourceSetupMandatesResponse::ErrorInformation(error_response) => Ok(Self { - response: { - let error_reason = &error_response.error_information.reason; - Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message, - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id.clone()), - }) - }, - status: enums::AttemptStatus::Failure, - ..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 + }) + } } } } @@ -1537,10 +2326,12 @@ impl )); 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(), ))), @@ -1591,6 +2382,7 @@ impl #[serde(rename_all = "camelCase")] pub struct CybersourceRefundRequest { order_information: OrderInformation, + client_reference_information: ClientReferenceInformation, } impl TryFrom<&CybersourceRouterData<&types::RefundsRouterData>> for CybersourceRefundRequest { @@ -1605,6 +2397,9 @@ impl TryFrom<&CybersourceRouterData<&types::RefundsRouterData>> for Cybers currency: item.router_data.request.currency, }, }, + client_reference_information: ClientReferenceInformation { + code: Some(item.router_data.request.refund_id.clone()), + }, }) } } @@ -1759,30 +2554,53 @@ pub struct AuthenticationErrorInformation { pub rmsg: String, } -impl From<(&Option, u16, String)> for types::ErrorResponse { +impl + From<( + &Option, + &Option, + u16, + String, + )> for types::ErrorResponse +{ fn from( - (error_data, status_code, transaction_id): ( + (error_data, risk_information, status_code, transaction_id): ( &Option, + &Option, u16, String, ), ) -> Self { - let error_message = error_data + let avs_message = risk_information .clone() - .and_then(|error_details| error_details.message); - + .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_reason + code: error_message .clone() .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason + message: error_message .clone() .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_message.clone(), + 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/nmi.rs b/crates/router/src/connector/nmi.rs index d514eefb10aa..0550908649ff 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -873,6 +873,21 @@ impl api::IncomingWebhook for Nmi { 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), ), diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 677bf303d95f..fcf35bfbe370 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -1166,21 +1166,19 @@ pub enum NmiWebhookEventType { impl ForeignFrom for webhooks::IncomingWebhookEvent { fn foreign_from(status: NmiWebhookEventType) -> Self { match status { - NmiWebhookEventType::SaleSuccess | NmiWebhookEventType::CaptureSuccess => { - Self::PaymentIntentSuccess - } - NmiWebhookEventType::SaleFailure | NmiWebhookEventType::CaptureFailure => { - Self::PaymentIntentFailure - } + 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::AuthSuccess - | NmiWebhookEventType::AuthFailure | NmiWebhookEventType::AuthUnknown - | NmiWebhookEventType::VoidFailure | NmiWebhookEventType::VoidUnknown | NmiWebhookEventType::CaptureUnknown => Self::EventNotSupported, } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 8c43e2c16a25..c151c5af455a 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -1977,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: @@ -2033,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 } diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 8875fdecfd08..89e186924142 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -3315,7 +3315,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, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 886de4174db4..8f028e37a9e5 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -117,7 +117,7 @@ where } enums::AttemptStatus::Charged => { let captured_amount = - types::Capturable::get_capture_amount(&self.request, payment_data); + 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 @@ -273,6 +273,7 @@ pub trait PaymentsPreProcessingData { fn get_webhook_url(&self) -> Result; fn get_return_url(&self) -> Result; fn get_browser_info(&self) -> Result; + fn get_complete_authorize_url(&self) -> Result; } impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { @@ -317,6 +318,11 @@ impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { .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 { @@ -592,6 +598,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 { @@ -616,6 +623,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 { @@ -806,7 +818,7 @@ impl CardData for api::Card { 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() ))) @@ -817,14 +829,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() )) @@ -1211,7 +1223,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() } @@ -1723,6 +1735,45 @@ impl FrmTransactionRouterDataRequest for fraud_check::FrmTransactionRouterData { } } +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)] @@ -1802,43 +1853,3 @@ mod error_code_error_message_tests { assert_eq!(error_code_error_message_none, 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, - } -} diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index 3641c0c3ddc3..39296bb64340 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -635,21 +635,44 @@ impl api::IncomingWebhook for Volt { &self, request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - let webhook_body: volt::VoltWebhookBodyReference = request - .body - .parse_struct("VoltWebhookBodyReference") - .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; - let reference = match webhook_body.merchant_internal_reference { - Some(merchant_internal_reference) => { - api_models::payments::PaymentIdType::PaymentAttemptId(merchant_internal_reference) - } - None => { - api_models::payments::PaymentIdType::ConnectorTransactionId(webhook_body.payment) - } - }; - Ok(api_models::webhooks::ObjectReferenceId::PaymentId( - reference, - )) + let x_volt_type = + utils::get_header_key_value(webhook_headers::X_VOLT_TYPE, request.headers)?; + if x_volt_type == "refund_confirmed" || x_volt_type == "refund_failed" { + let refund_webhook_body: volt::VoltRefundWebhookBodyReference = request + .body + .parse_struct("VoltRefundWebhookBodyReference") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + + let refund_reference = match refund_webhook_body.external_reference { + Some(external_reference) => { + api_models::webhooks::RefundIdType::RefundId(external_reference) + } + None => api_models::webhooks::RefundIdType::ConnectorRefundId( + refund_webhook_body.refund, + ), + }; + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + refund_reference, + )) + } else { + let webhook_body: volt::VoltPaymentWebhookBodyReference = request + .body + .parse_struct("VoltPaymentWebhookBodyReference") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + let reference = match webhook_body.merchant_internal_reference { + Some(merchant_internal_reference) => { + api_models::payments::PaymentIdType::PaymentAttemptId( + merchant_internal_reference, + ) + } + None => api_models::payments::PaymentIdType::ConnectorTransactionId( + webhook_body.payment, + ), + }; + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + reference, + )) + } } fn get_webhook_event_type( @@ -663,7 +686,7 @@ impl api::IncomingWebhook for Volt { .body .parse_struct("VoltWebhookBodyEventType") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - Ok(api::IncomingWebhookEvent::from(payload.status)) + Ok(api::IncomingWebhookEvent::from(payload)) } } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index 4c6eaeb52f48..8b9bbecb0889 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -46,6 +46,7 @@ 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"; + pub const X_VOLT_TYPE: &str = "X-Volt-Type"; } #[derive(Debug, Serialize)] @@ -318,8 +319,8 @@ pub enum VoltPaymentStatus { #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum VoltPaymentsResponseData { - WebhookResponse(VoltWebhookObjectResource), PsyncResponse(VoltPsyncResponse), + WebhookResponse(VoltPaymentWebhookObjectResource), } #[derive(Debug, Serialize, Clone, Deserialize)] @@ -418,13 +419,16 @@ impl } } } - -impl From for enums::AttemptStatus { - fn from(status: VoltWebhookStatus) -> Self { +impl From for enums::AttemptStatus { + fn from(status: VoltWebhookPaymentStatus) -> Self { match status { - VoltWebhookStatus::Completed | VoltWebhookStatus::Received => Self::Charged, - VoltWebhookStatus::Failed | VoltWebhookStatus::NotReceived => Self::Failure, - VoltWebhookStatus::Pending => Self::Pending, + VoltWebhookPaymentStatus::Completed | VoltWebhookPaymentStatus::Received => { + Self::Charged + } + VoltWebhookPaymentStatus::Failed | VoltWebhookPaymentStatus::NotReceived => { + Self::Failure + } + VoltWebhookPaymentStatus::Pending => Self::Pending, } } } @@ -432,6 +436,7 @@ impl From for enums::AttemptStatus { // REFUND : // Type definition for RefundRequest #[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct VoltRefundRequest { pub amount: i64, pub external_reference: String, @@ -447,28 +452,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, @@ -492,30 +475,66 @@ impl TryFrom> } #[derive(Debug, Deserialize, Clone, Serialize)] -pub struct VoltWebhookBodyReference { +#[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, Serialize)] +#[serde(untagged)] +pub enum VoltWebhookBodyEventType { + Payment(VoltPaymentsWebhookBodyEventType), + Refund(VoltRefundsWebhookBodyEventType), +} + #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct VoltWebhookBodyEventType { - pub status: VoltWebhookStatus, +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 VoltWebhookObjectResource { +pub struct VoltPaymentWebhookObjectResource { pub payment: String, pub merchant_internal_reference: Option, - pub status: VoltWebhookStatus, + 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 VoltWebhookStatus { +pub enum VoltWebhookPaymentStatus { Completed, Failed, Pending, @@ -523,6 +542,13 @@ pub enum VoltWebhookStatus { 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)] @@ -539,16 +565,22 @@ pub enum VoltDetailedStatus { AwaitingCheckoutAuthorisation, } -impl From for api::IncomingWebhookEvent { - fn from(status: VoltWebhookStatus) -> Self { +impl From for api::IncomingWebhookEvent { + fn from(status: VoltWebhookBodyEventType) -> Self { match status { - VoltWebhookStatus::Completed | VoltWebhookStatus::Received => { - Self::PaymentIntentSuccess - } - VoltWebhookStatus::Failed | VoltWebhookStatus::NotReceived => { - Self::PaymentIntentFailure - } - VoltWebhookStatus::Pending => Self::PaymentIntentProcessing, + 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, + }, } } } diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index c00913aa57d1..c55663d59f48 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -342,7 +342,7 @@ 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(), diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index afe761846304..ed020b0c7e0f 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -27,6 +27,9 @@ 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"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 0bd197ee22e9..5ae4b0be33da 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -1,6 +1,7 @@ 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; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 2577bb83a3a2..e8593581126a 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -10,6 +10,7 @@ use common_utils::{ ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, pii, }; +use diesel_models::configs; use error_stack::{report, FutureExt, IntoReport, ResultExt}; use futures::future::try_join_all; use masking::{PeekInterface, Secret}; @@ -141,6 +142,17 @@ pub async fn create_merchant_account( .transpose()? .map(Secret::new); + 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, + }) + .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) .await 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/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index f94504cf274d..54ec4ec1e295 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -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")] diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index fa9a5185790d..ff764cafed62 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -187,6 +187,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)) } diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs index e0efccdc9125..0e3f67c051b8 100644 --- a/crates/router/src/core/fraud_check.rs +++ b/crates/router/src/core/fraud_check.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use api_models::{admin::FrmConfigs, enums as api_enums, payments::AdditionalPaymentData}; use error_stack::ResultExt; -use masking::PeekInterface; +use masking::{ExposeInterface, PeekInterface}; use router_env::{ logger, tracing::{self, instrument}, @@ -167,10 +167,9 @@ where match frm_configs_option { Some(frm_configs_value) => { let frm_configs_struct: Vec = frm_configs_value - .iter() + .into_iter() .map(|config| { config - .peek() - .clone() + .expose() .parse_value("FrmConfigs") .change_context(errors::ApiErrorResponse::InvalidDataFormat { field_name: "frm_configs".to_string(), diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index f2043d392ab2..84cd726a7e49 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,7 +1,7 @@ use api_models::admin as admin_types; use common_utils::{ consts::{ - DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, + DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY, }, ext_traits::{OptionExt, ValueExt}, @@ -13,7 +13,6 @@ use time::PrimitiveDateTime; use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ - core::payments::helpers, errors::RouterResponse, routes::AppState, services, @@ -68,18 +67,6 @@ 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, - ], - "use payment link for", - )?; - let merchant_name_from_merchant_account = merchant_account .merchant_name .clone() @@ -98,10 +85,11 @@ pub async fn intiate_payment_link_flow( 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 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 @@ -114,23 +102,73 @@ 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 order_details = validate_order_details(payment_intent.order_details, currency)?; + 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(|| { - common_utils::date_time::now() + payment_intent + .created_at .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY)) }); // 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); + + if 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, + ], + ) || payment_link_status == 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.clone().logo, + created: payment_link.created_at, + intent_status: payment_intent.status, + payment_link_status, + error_code: payment_attempt.error_code, + error_message: payment_attempt.error_message, + }; + 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: currency - .to_currency_base_unit(payment_intent.amount) - .into_report() - .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?, + amount, currency, payment_id: payment_intent.payment_id, merchant_name, @@ -143,17 +181,19 @@ pub async fn intiate_payment_link_flow( max_items_visible_after_collapse: 3, theme: payment_link_config.clone().theme, merchant_description: payment_intent.description, + sdk_layout: payment_link_config.clone().sdk_layout, }; - let js_script = get_js_script(payment_details)?; - let css_script = get_color_scheme_css(payment_link_config.clone()); + 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), ))) } @@ -161,13 +201,11 @@ 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};")) } @@ -218,11 +256,11 @@ pub async fn list_payment_link( } pub fn check_payment_link_status( - max_age: PrimitiveDateTime, + payment_link_expiry: PrimitiveDateTime, ) -> api_models::payments::PaymentLinkStatus { let curr_time = common_utils::date_time::now(); - if curr_time > max_age { + if curr_time > payment_link_expiry { api_models::payments::PaymentLinkStatus::Expired } else { api_models::payments::PaymentLinkStatus::Active @@ -348,10 +386,21 @@ pub fn get_payment_link_config_based_on_priority( }) .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)) @@ -369,3 +418,10 @@ fn capitalize_first_char(s: &str) -> String { 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) +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index 4fb5bb98efe6..f6e62f8bdc8a 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -63,7 +63,6 @@ width: 100vw; display: flex; justify-content: center; - background-color: white; padding: 20px 0; } @@ -133,7 +132,6 @@ #hyper-checkout-cart-image { height: 64px; width: 64px; - border: 1px solid #e6e6e6; border-radius: 4px; display: flex; align-self: flex-start; @@ -344,10 +342,6 @@ font-size: 25px; } - .payNow { - margin-top: 10px; - } - .page-spinner { position: absolute; width: 100vw; @@ -605,9 +599,38 @@ 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: scroll; + overflow-y: scroll; } .hyper-checkout { @@ -720,6 +743,7 @@ background-color: transparent; width: auto; min-width: 300px; + box-shadow: none; } #payment-form-wrap { @@ -748,7 +772,7 @@ href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800" /> - + @@ -920,7 +944,7 @@
-
+
-
+
+
@@ -1060,7 +1088,7 @@ window.state = { prevHeight: window.innerHeight, prevWidth: window.innerWidth, - isMobileView: window.innerWidth <= 1200, + isMobileView: window.innerWidth <= 1400, currentScreen: "payment_link", }; @@ -1088,9 +1116,9 @@ } // Render UI - renderPaymentDetails(); - renderSDKHeader(); - renderCart(); + renderPaymentDetails(paymentDetails); + renderSDKHeader(paymentDetails); + renderCart(paymentDetails); // Deal w loaders show("#sdk-spinner"); @@ -1098,7 +1126,7 @@ hide("#unified-checkout"); // Add event listeners - initializeEventListeners(); + initializeEventListeners(paymentDetails); // Initialize SDK if (window.Hyper) { @@ -1115,30 +1143,46 @@ } boot(); - function initializeEventListeners() { - var primaryColor = window - .getComputedStyle(document.documentElement) - .getPropertyValue("--primary-color"); + 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 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 statusRedirectTextNode = document.getElementById( "hyper-checkout-status-redirect-message" ); + var submitButtonNode = document.getElementById("submit"); + var submitButtonLoaderNode = document.getElementById("submit-spinner"); + + if (submitButtonLoaderNode instanceof HTMLSpanElement) { + submitButtonLoaderNode.style.borderBottomColor = contrastingTone; + } - if (window.innerWidth <= 1200) { + if (submitButtonNode instanceof HTMLButtonElement) { + submitButtonNode.style.color = contrastBWColor; + } + + if (hyperCheckoutCartImageNode instanceof HTMLDivElement) { + hyperCheckoutCartImageNode.style.backgroundColor = contrastingTone; + } + + if (window.innerWidth <= 1400) { statusRedirectTextNode.style.color = "#333333"; hyperCheckoutNode.style.color = contrastBWColor; var a = lighterColor.match(/[fF]/gi); - hyperCheckoutFooterNode.style.backgroundColor = - Array.isArray(a) && a.length > 4 ? darkerColor : lighterColor; - } else if (window.innerWidth > 1200) { + hyperCheckoutFooterNode.style.backgroundColor = contrastingTone; + } else if (window.innerWidth > 1400) { statusRedirectTextNode.style.color = contrastBWColor; hyperCheckoutNode.style.color = "#333333"; hyperCheckoutFooterNode.style.backgroundColor = "#F5F5F5"; @@ -1147,7 +1191,7 @@ window.addEventListener("resize", function (event) { var currentHeight = window.innerHeight; var currentWidth = window.innerWidth; - if (currentWidth <= 1200 && window.state.prevWidth > 1200) { + if (currentWidth <= 1400 && window.state.prevWidth > 1400) { hide("#hyper-checkout-cart"); if (window.state.currentScreen === "payment_link") { show("#hyper-footer"); @@ -1162,7 +1206,7 @@ error ); } - } else if (currentWidth > 1200 && window.state.prevWidth <= 1200) { + } else if (currentWidth > 1400 && window.state.prevWidth <= 1400) { if (window.state.currentScreen === "payment_link") { hide("#hyper-footer"); } @@ -1178,16 +1222,17 @@ window.state.prevHeight = currentHeight; window.state.prevWidth = currentWidth; - window.state.isMobileView = currentWidth <= 1200; + window.state.isMobileView = currentWidth <= 1400; }); } - function showSDK() { - checkStatus() + function showSDK(paymentDetails) { + checkStatus(paymentDetails) .then(function (res) { if (res.showSdk) { show("#hyper-checkout-sdk"); show("#hyper-checkout-details"); + show("#submit"); } else { hide("#hyper-checkout-details"); hide("#hyper-checkout-sdk"); @@ -1195,6 +1240,8 @@ hide("#hyper-footer"); window.state.currentScreen = "status"; } + show("#unified-checkout"); + hide("#sdk-spinner"); }) .catch(function (err) { console.error("Failed to check status", err); @@ -1217,14 +1264,20 @@ colorBackground: "rgb(255, 255, 255)", }, }; - hyper = window.Hyper(pub_key); + 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 = { - layout: "tabs", - sdkHandleConfirmPayment: true, + layout: { + type: type, //accordion , tabs, spaced accordion + spacedAccordionItems: paymentDetails.sdk_layout === "spaced_accordion" + }, branding: "never", wallets: { walletReturnUrl: paymentDetails.return_url, @@ -1237,35 +1290,7 @@ }; unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions); mountUnifiedCheckout("#unified-checkout"); - - // Add event listener for SDK iframe mutations - var orcaIFrame = document.getElementById( - "orca-payment-element-iframeRef-unified-checkout" - ); - var callback = function (mutationList, observer) { - for (var i = 0; i < mutationList.length; i++) { - var mutation = mutationList[i]; - - if ( - mutation.type === "attributes" && - mutation.attributeName === "style" - ) { - show("#unified-checkout"); - hide("#sdk-spinner"); - } - } - }; - var observer = new MutationObserver(callback); - observer.observe(orcaIFrame, { attributes: true }); - - // Handle button press callback - var paymentElement = widgets.getElement("payment"); - if (paymentElement) { - paymentElement.on("confirmTriggered", function (event) { - handleSubmit(event); - }); - } - showSDK(); + showSDK(paymentDetails); } // Util functions @@ -1277,6 +1302,14 @@ function handleSubmit(e) { var paymentDetails = window.__PAYMENT_DETAILS; + + // Update button loader + hide("#submit-button-text"); + show("#submit-spinner"); + var submitButtonNode = document.getElementById("submit"); + submitButtonNode.disabled = true; + submitButtonNode.classList.add("disabled"); + hyper .confirmPayment({ widgets: widgets, @@ -1293,9 +1326,6 @@ } else { showMessage("An unexpected error occurred."); } - - // Re-initialize SDK - mountUnifiedCheckout("#unified-checkout"); } else { // This point will only be reached if there is an immediate error occurring while confirming the payment. Otherwise, your customer will be redirected to your 'return_url'. // For some payment flows such as Sofort, iDEAL, your customer will be redirected to an intermediate page to complete authorization of the payment, and then redirected to the 'return_url'. @@ -1320,13 +1350,18 @@ }) .catch(function (error) { console.error("Error confirming payment_intent", error); + }) + .finally(() => { + hide("#submit-spinner"); + show("#submit-button-text"); + submitButtonNode.disabled = false; + submitButtonNode.classList.remove("disabled"); }); } // Fetches the payment status after payment submission - function checkStatus() { + function checkStatus(paymentDetails) { return new window.Promise(function (resolve, reject) { - var paymentDetails = window.__PAYMENT_DETAILS; var res = { showSdk: true, }; @@ -1669,9 +1704,7 @@ return formatted; } - function renderPaymentDetails() { - var paymentDetails = window.__PAYMENT_DETAILS; - + function renderPaymentDetails(paymentDetails) { // Create price node var priceNode = document.createElement("div"); priceNode.className = "hyper-checkout-payment-price"; @@ -1720,8 +1753,7 @@ footerNode.append(paymentExpiryNode); } - function renderCart() { - var paymentDetails = window.__PAYMENT_DETAILS; + function renderCart(paymentDetails) { var orderDetails = paymentDetails.order_details; // Cart items @@ -1749,7 +1781,9 @@ if (totalItems > MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { var expandButtonNode = document.createElement("div"); expandButtonNode.className = "hyper-checkout-cart-button"; - expandButtonNode.onclick = handleCartView; + expandButtonNode.onclick = () => { + handleCartView(paymentDetails); + }; var buttonImageNode = document.createElement("svg"); buttonImageNode.id = "hyper-checkout-cart-button-arrow"; buttonImageNode.innerHTML = @@ -1822,8 +1856,7 @@ cartItemsNode.append(itemWrapperNode); } - function handleCartView() { - var paymentDetails = window.__PAYMENT_DETAILS; + function handleCartView(paymentDetails) { var orderDetails = paymentDetails.order_details; var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = paymentDetails.max_items_visible_after_collapse; @@ -1911,9 +1944,7 @@ show("#hyper-checkout-cart"); } - function renderSDKHeader() { - var paymentDetails = window.__PAYMENT_DETAILS; - + function renderSDKHeader(paymentDetails) { // SDK headers' items var sdkHeaderItemNode = document.createElement("div"); sdkHeaderItemNode.className = "hyper-checkout-sdk-items"; diff --git a/crates/router/src/core/payment_link/status.html b/crates/router/src/core/payment_link/status.html new file mode 100644 index 000000000000..d3bb97d294dd --- /dev/null +++ b/crates/router/src/core/payment_link/status.html @@ -0,0 +1,355 @@ + + + + + 404 Not Found + + + + + + +
+
+
+
+
+
+ + diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index c71632c9b06d..070bca234c8e 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, @@ -53,7 +47,7 @@ impl Vaultable for api::Card { exp_month: self.card_exp_month.peek().clone(), name_on_card: self .card_holder_name - .clone() + .as_ref() .map(|name| name.peek().clone()), nickname: None, card_last_four: None, @@ -806,11 +800,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) } } @@ -874,11 +863,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) } } @@ -925,11 +909,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) } } @@ -1056,246 +1035,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 df7d3a12de80..5b6789dad0b0 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -44,7 +44,7 @@ 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, @@ -537,8 +537,7 @@ where let raw_card_key = payment_data .payment_method_data .as_ref() - .map(get_key_params_for_surcharge_details) - .transpose()? + .and_then(get_key_params_for_surcharge_details) .map(|(payment_method, payment_method_type, card_network)| { types::SurchargeKey::PaymentMethodData( payment_method, @@ -1519,6 +1518,22 @@ where router_data = router_data.preprocessing_steps(state, connector).await?; (router_data, false) + } else if connector.connector_name == router_types::Connector::Cybersource + && 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) } @@ -1612,6 +1627,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); @@ -1625,13 +1641,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 => { @@ -1642,9 +1680,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 @@ -1815,19 +1853,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(), @@ -2115,6 +2152,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, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 27ddd3f6d81c..6dd692f15259 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -154,7 +154,6 @@ default_imp_for_complete_authorize!( connector::Checkout, connector::Coinbase, connector::Cryptopay, - connector::Cybersource, connector::Dlocal, connector::Fiserv, connector::Forte, @@ -873,7 +872,6 @@ default_imp_for_pre_processing_steps!( connector::Checkout, connector::Coinbase, connector::Cryptopay, - connector::Cybersource, connector::Dlocal, connector::Iatapay, connector::Fiserv, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index c934c7c2cd67..07af15a336d9 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -412,6 +412,7 @@ impl TryFrom for types::PaymentsPreProcessingData browser_info: data.browser_info, surcharge_details: data.surcharge_details, connector_transaction_id: None, + redirect_response: None, }) } } @@ -431,10 +432,11 @@ impl TryFrom for types::PaymentsPreProcessingData order_details: None, router_return_url: None, webhook_url: None, - complete_authorize_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 2d52a145feae..68d0ee8d475f 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -203,10 +203,19 @@ pub async fn complete_authorize_preprocessing_steps( ], ); + 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.to_owned(), + router_data_request, resp.response, ); diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index d864cacc52fd..7230d74e9a98 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2586,6 +2586,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()), @@ -2638,6 +2639,7 @@ mod tests { statement_descriptor_suffix: None, 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, @@ -2695,6 +2697,7 @@ mod tests { 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, @@ -3287,6 +3290,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() @@ -3306,6 +3310,7 @@ pub async fn get_additional_payment_data( 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 { @@ -3329,6 +3334,7 @@ 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: card_data.card_holder_name.clone(), @@ -3344,6 +3350,7 @@ pub async fn get_additional_payment_data( 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(), @@ -3610,93 +3617,74 @@ impl ApplePayData { pub fn get_key_params_for_surcharge_details( payment_method_data: &api_models::payments::PaymentMethodData, -) -> RouterResult<( +) -> Option<( common_enums::PaymentMethod, common_enums::PaymentMethodType, Option, )> { match payment_method_data { api_models::payments::PaymentMethodData::Card(card) => { - let card_network = card - .card_network - .clone() - .get_required_value("payment_method_data.card.card_network")?; // surcharge generated will always be same for credit as well as debit // since surcharge conditions cannot be defined on card_type - Ok(( + Some(( common_enums::PaymentMethod::Card, common_enums::PaymentMethodType::Credit, - Some(card_network), + card.card_network.clone(), )) } - api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Ok(( + 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) => Ok(( + api_models::payments::PaymentMethodData::Wallet(wallet) => Some(( common_enums::PaymentMethod::Wallet, wallet.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::PayLater(pay_later) => Ok(( + 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) => Ok(( + 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) => Ok(( + 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) => Ok(( + 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) => Ok(( + api_models::payments::PaymentMethodData::Crypto(crypto) => Some(( common_enums::PaymentMethod::Crypto, crypto.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::MandatePayment => { - Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_method_data", - } - .into()) - } - api_models::payments::PaymentMethodData::Reward => { - Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_method_data", - } - .into()) - } - api_models::payments::PaymentMethodData::Upi(_) => Ok(( + 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) => Ok(( + api_models::payments::PaymentMethodData::Voucher(voucher) => Some(( common_enums::PaymentMethod::Voucher, voucher.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::GiftCard(gift_card) => Ok(( + api_models::payments::PaymentMethodData::GiftCard(gift_card) => Some(( common_enums::PaymentMethod::GiftCard, gift_card.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::CardToken(_) => { - Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_method_data", - } - .into()) - } + api_models::payments::PaymentMethodData::CardToken(_) => None, } } @@ -3761,17 +3749,22 @@ pub async fn get_gsm_record( pub fn validate_order_details_amount( order_details: Vec, amount: i64, + should_validate: bool, ) -> Result<(), errors::ApiErrorResponse> { - let total_order_details_amount: i64 = order_details - .iter() - .map(|order| order.amount * i64::from(order.quantity)) - .sum(); + 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(), - }) + 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(()) } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 0970a952c8e0..c81145c5de72 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -2,23 +2,30 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; -use common_utils::ext_traits::{AsyncExt, Encode}; +use common_utils::{ + crypto::{self, SignMessage}, + ext_traits::{AsyncExt, Encode}, +}; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::kms; use futures::FutureExt; use router_derive::PaymentOperation; -use router_env::{instrument, tracing}; +use router_env::{instrument, logger, tracing}; use tracing_futures::Instrument; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ + consts, core::{ + blocklist::utils as blocklist_utils, errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, payments::{ self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, }, - utils::{self as core_utils}, + utils as core_utils, }, db::StorageInterface, routes::AppState, @@ -104,6 +111,7 @@ impl helpers::validate_order_details_amount( order_details.to_owned(), payment_intent.amount, + false, )?; } @@ -619,32 +627,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(); @@ -708,6 +718,157 @@ 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() @@ -788,6 +949,7 @@ impl metadata: m_metadata, payment_confirm_source: header_payload.payment_confirm_source, updated_by: m_storage_scheme, + fingerprint_id, session_expiry, }, storage_scheme, @@ -837,6 +999,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)) } } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 94436026dc4a..2b25a74deb19 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -245,6 +245,7 @@ impl helpers::validate_order_details_amount( order_details.to_owned(), payment_intent.amount, + false, )?; } @@ -824,6 +825,7 @@ impl PaymentCreate { request_incremental_authorization, incremental_authorization_allowed: None, authorization_count: None, + fingerprint_id: None, session_expiry: Some(session_expiry), }) } diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index d623a706f419..e5552f0d156d 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -24,7 +24,7 @@ use crate::{ services::RedirectForm, types::{ self, api, - storage::{self, enums, payment_attempt::AttemptStatusExt}, + storage::{self, enums}, transformers::{ForeignFrom, ForeignTryFrom}, CaptureSyncResponse, }, @@ -499,15 +499,9 @@ 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), @@ -641,6 +635,9 @@ async fn payment_response_update_tracker( 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 @@ -654,15 +651,6 @@ async fn payment_response_update_tracker( 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 - }, updated_by: storage_scheme.to_string(), authentication_data, encoded_data, @@ -925,7 +913,7 @@ fn get_total_amount_captured( } None => { //Non multiple capture - let amount = request.get_capture_amount(payment_data); + 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_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 5ed0c45d4e26..e002b92d1810 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -64,6 +64,7 @@ impl helpers::validate_order_details_amount( order_details.to_owned(), payment_intent.amount, + false, )?; } @@ -616,6 +617,7 @@ impl metadata, payment_confirm_source: None, updated_by: storage_scheme.to_string(), + fingerprint_id: None, session_expiry, }, storage_scheme, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 7b7d64a5f81a..dffcff23595b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -118,7 +118,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 { @@ -706,6 +706,7 @@ where .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) .to_owned(), @@ -1223,6 +1224,7 @@ impl TryFrom> for types::PaymentsCaptureD None => None, }, browser_info, + metadata: payment_data.payment_intent.metadata, }) } } @@ -1257,6 +1259,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, }) } } @@ -1422,6 +1425,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 @@ -1443,7 +1449,11 @@ impl TryFrom> for types::CompleteAuthoriz .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(), @@ -1460,6 +1470,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, }) } } @@ -1538,6 +1550,7 @@ impl TryFrom> for types::PaymentsPreProce 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/refunds.rs b/crates/router/src/core/refunds.rs index 6cc118b0f3c7..e60c341dedcf 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -650,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 @@ -776,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, } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 532f8208ecf1..b1a582cedecf 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,7 +1,5 @@ use api_models::user as user_api; -#[cfg(feature = "email")] -use diesel_models::user_role::UserRoleNew; -use diesel_models::{enums::UserStatus, user as storage_user}; +use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew}; #[cfg(feature = "email")] use error_stack::IntoReport; use error_stack::ResultExt; @@ -342,7 +340,6 @@ pub async fn reset_password( Ok(ApplicationResponse::StatusOk) } -#[cfg(feature = "email")] pub async fn invite_user( state: AppState, request: user_api::InviteUserRequest, @@ -395,6 +392,7 @@ pub async fn invite_user( Ok(ApplicationResponse::Json(user_api::InviteUserResponse { is_email_sent: false, + password: None, })) } else if invitee_user .as_ref() @@ -432,25 +430,37 @@ pub async fn invite_user( } })?; - 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!", - }; - - 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); + 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!", + }; + 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: send_email_result.is_ok(), + is_email_sent, + password: if cfg!(not(feature = "email")) { + Some(new_user.get_password().get_secret()) + } else { + None + }, })) } else { Err(UserErrors::InternalServerError.into()) diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 5beace9cbb83..b9d346b7a71f 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -1,6 +1,9 @@ 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; @@ -68,6 +71,7 @@ pub trait StorageInterface: + dyn_clone::DynClone + address::AddressInterface + api_keys::ApiKeyInterface + + blocklist_lookup::BlocklistLookupInterface + configs::ConfigInterface + capture::CaptureInterface + customers::CustomerInterface @@ -85,6 +89,8 @@ pub trait StorageInterface: + PaymentAttemptInterface + PaymentIntentInterface + payment_method::PaymentMethodInterface + + blocklist::BlocklistInterface + + blocklist_fingerprint::BlocklistFingerprintInterface + scheduler::SchedulerInterface + payout_attempt::PayoutAttemptInterface + payouts::PayoutsInterface 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/lib.rs b/crates/router/src/lib.rs index 3b4c7ce9b7d3..696198f2153c 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -129,9 +129,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::PaymentMethods::server(state.clone())) } #[cfg(feature = "olap")] @@ -143,6 +143,7 @@ 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())) diff --git a/crates/router/src/macros.rs b/crates/router/src/macros.rs index e6c9dba7d6e2..efe71e49bb04 100644 --- a/crates/router/src/macros.rs +++ b/crates/router/src/macros.rs @@ -1,4 +1 @@ -pub use common_utils::{ - async_spawn, collect_missing_value_keys, fallback_reverse_lookup_not_found, newtype, - newtype_impl, -}; +pub use common_utils::{collect_missing_value_keys, newtype}; diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 79b38e03f31d..174926c7d360 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -119,6 +119,9 @@ Never share your secret api keys. Keep them guarded and secure. crate::routes::gsm::get_gsm_rule, crate::routes::gsm::update_gsm_rule, crate::routes::gsm::delete_gsm_rule, + crate::routes::blocklist::add_entry_to_blocklist, + crate::routes::blocklist::list_blocked_payment_methods, + crate::routes::blocklist::remove_entry_from_blocklist ), components(schemas( crate::types::api::refunds::RefundRequest, @@ -370,7 +373,11 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentLinkResponse, api_models::payments::RetrievePaymentLinkResponse, api_models::payments::PaymentLinkInitiateRequest, - api_models::payments::PaymentLinkStatus + api_models::payments::PaymentLinkStatus, + api_models::blocklist::BlocklistRequest, + api_models::blocklist::BlocklistResponse, + api_models::blocklist::ListBlocklistQuery, + common_enums::enums::BlocklistDataKind )), modifiers(&SecurityAddon) )] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index ec718b2dde9f..d4bfabb6f92a 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -1,6 +1,8 @@ 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; @@ -42,14 +44,15 @@ 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 = "kms"))] pub use self::app::Verify; pub use self::app::{ diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 6625a206be21..0b2acaf4e506 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -14,6 +14,8 @@ 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")] @@ -566,6 +568,23 @@ impl PaymentMethods { } } +#[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)), + ) + } +} + pub struct MerchantAccount; #[cfg(feature = "olap")] @@ -879,6 +898,7 @@ impl User { .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) .service(web::resource("/role/list").route(web::get().to(list_roles))) .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) + .service(web::resource("/user/invite").route(web::post().to(invite_user))) .service( web::resource("/data") .route(web::get().to(get_multiple_dashboard_metadata)) @@ -901,7 +921,6 @@ impl User { ) .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("/user/invite").route(web::post().to(invite_user))) .service( web::resource("/signup_with_merchant_id") .route(web::post().to(user_signup_with_merchant_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/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 10f408f3d4f0..55c6cbc23d70 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -24,6 +24,7 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + Blocklist, Forex, RustLockerMigration, Gsm, @@ -57,6 +58,10 @@ impl From for ApiIdentifier { Flow::RetrieveForexFlow => Self::Forex, + Flow::AddToBlocklist => Self::Blocklist, + Flow::DeleteFromBlocklist => Self::Blocklist, + Flow::ListBlocklist => Self::Blocklist, + Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve | Flow::MerchantConnectorsUpdate diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 43a7272a4435..a6eeeabd687f 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -108,7 +108,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."), @@ -134,10 +133,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, @@ -153,7 +148,7 @@ pub async fn list_customer_payment_method_api( Some(&customer_id), ) }, - &*auth, + &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, )) .await @@ -166,7 +161,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."), diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 7f0f0db3b69e..a77b82c550e6 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -333,7 +333,6 @@ pub async fn reset_password( .await } -#[cfg(feature = "email")] pub async fn invite_user( state: web::Data, req: HttpRequest, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 54df02855120..9eb06d675a07 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -733,11 +733,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, @@ -745,6 +751,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, @@ -777,6 +789,15 @@ 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, @@ -1051,16 +1072,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" + } + }"#, + ), + } + } } } @@ -1398,6 +1435,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) @@ -1634,6 +1770,26 @@ fn get_hyper_loader_sdk(sdk_url: &str) -> String { format!("") } +pub fn get_payment_link_status( + payment_link_data: PaymentLinkStatusData, +) -> CustomResult { + let html_template = include_str!("../core/payment_link/status.html").to_string(); + let mut tera = Tera::default(); + let _ = tera.add_raw_template("payment_link_status", &html_template); + + let mut context = Context::new(); + context.insert("css_color_scheme", &payment_link_data.css_script); + context.insert("payment_details_js_script", &payment_link_data.js_script); + + 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] diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index fca85c41699a..f4d74c4f81bb 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -112,7 +112,6 @@ pub(super) fn create_client( pub fn proxy_bypass_urls(locker: &Locker) -> Vec { let locker_host = locker.host.to_owned(); let locker_host_rs = locker.host_rs.to_owned(); - let basilisk_host = locker.basilisk_host.to_owned(); vec![ format!("{locker_host}/cards/add"), format!("{locker_host}/cards/retrieve"), @@ -124,10 +123,6 @@ pub fn proxy_bypass_urls(locker: &Locker) -> Vec { format!("{locker_host}/card/addCard"), format!("{locker_host}/card/getCard"), format!("{locker_host}/card/deleteCard"), - format!("{basilisk_host}/tokenize"), - format!("{basilisk_host}/tokenize/get"), - format!("{basilisk_host}/tokenize/delete"), - format!("{basilisk_host}/tokenize/delete/token"), ] } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 2225c2965bcf..e236113e6768 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -428,6 +428,8 @@ 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)] @@ -488,6 +490,7 @@ pub struct PaymentsPreProcessingData { pub surcharge_details: Option, pub browser_info: Option, pub connector_transaction_id: Option, + pub redirect_response: Option, } #[derive(Debug, Clone)] @@ -508,6 +511,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)] @@ -542,6 +547,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)] @@ -592,7 +599,17 @@ pub struct AccessTokenRequestData { } pub trait Capturable { - fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + fn get_captured_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { + None + } + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + _attempt_status: common_enums::AttemptStatus, + ) -> Option where F: Clone, { @@ -601,7 +618,7 @@ pub trait Capturable { } impl Capturable for PaymentsAuthorizeData { - fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + fn get_captured_amount(&self, _payment_data: &PaymentData) -> Option where F: Clone, { @@ -611,41 +628,171 @@ impl Capturable for PaymentsAuthorizeData { .map(|surcharge_details| surcharge_details.final_amount); final_amount.or(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 PaymentsCaptureData { - fn get_capture_amount(&self, _payment_data: &PaymentData) -> 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, _payment_data: &PaymentData) -> 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 { - fn get_capture_amount(&self, payment_data: &PaymentData) -> Option + 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 PaymentsIncrementalAuthorizationData {} +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_capture_amount(&self, payment_data: &PaymentData) -> Option + fn get_captured_amount(&self, payment_data: &PaymentData) -> Option where F: Clone, { @@ -654,6 +801,20 @@ impl Capturable for PaymentsSyncData { .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 { diff --git a/crates/router/src/types/api/payment_link.rs b/crates/router/src/types/api/payment_link.rs index d0ce8c043baa..85cb539d4118 100644 --- a/crates/router/src/types/api/payment_link.rs +++ b/crates/router/src/types/api/payment_link.rs @@ -15,7 +15,8 @@ pub(crate) trait PaymentLinkResponseExt: Sized { 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(|| { - common_utils::date_time::now() + payment_link + .created_at .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY)) }); let status = payment_link::check_payment_link_status(session_expiry); diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 8f204814ec40..d271ed5e29d1 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -489,6 +489,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, @@ -683,8 +687,7 @@ impl TryFrom for NewUser { 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 = password::generate_password_hash(uuid::Uuid::new_v4().to_string().into())?; - let password = UserPassword::new(password)?; + let password = UserPassword::new(uuid::Uuid::new_v4().to_string().into())?; let new_merchant = NewUserMerchant::try_from(value)?; Ok(Self { diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 56d3272b9471..b93cbbbbba92 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -1,6 +1,9 @@ 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; @@ -43,7 +46,8 @@ pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate} pub use scheduler::db::process_tracker; pub use self::{ - address::*, api_keys::*, authorization::*, capture::*, cards_info::*, configs::*, customers::*, + 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::*, 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/transformers.rs b/crates/router/src/types/transformers.rs index c3818caf051a..786a8c551824 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -352,11 +352,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 - | api_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + | api_enums::IntentStatus::RequiresConfirmation => None, } } } diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 33f1e2115349..dcf635595e0f 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -199,6 +199,7 @@ pub async fn generate_sample_data( 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 { diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index ff179f745065..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" diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index 3c7ba8b93df7..0127d07170fd 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -52,6 +52,7 @@ pub enum AnalyticsFlow { GenerateRefundReport, GetApiEventMetrics, GetApiEventFilters, + GetOutgoingWebhookEvents, } impl FlowMetric for AnalyticsFlow {} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index e37e15443bdb..a6ac1b1e0a14 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -185,6 +185,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 diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index 50173bb1c739..ac3a04e85b2b 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")] @@ -103,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, diff --git a/crates/storage_impl/src/mock_db/payment_intent.rs b/crates/storage_impl/src/mock_db/payment_intent.rs index ee8676106f1d..3f892ed9fa7a 100644 --- a/crates/storage_impl/src/mock_db/payment_intent.rs +++ b/crates/storage_impl/src/mock_db/payment_intent.rs @@ -109,6 +109,7 @@ impl PaymentIntentInterface for MockDb { 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()); diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 07d70c9056b7..8d20dfe0f32b 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -101,6 +101,7 @@ impl PaymentIntentInterface for KVRouterStore { 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 { @@ -769,6 +770,7 @@ impl DataModelExt for PaymentIntentNew { 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, } } @@ -813,6 +815,7 @@ impl DataModelExt for PaymentIntentNew { 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, } } @@ -862,6 +865,7 @@ impl DataModelExt for PaymentIntent { 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, } } @@ -907,6 +911,7 @@ impl DataModelExt for PaymentIntent { 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, } } @@ -990,6 +995,7 @@ impl DataModelExt for PaymentIntentUpdate { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, } => DieselPaymentIntentUpdate::Update { amount, @@ -1009,6 +1015,7 @@ impl DataModelExt for PaymentIntentUpdate { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, }, Self::PaymentAttemptAndAttemptCountUpdate { 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/loadtest/config/development.toml b/loadtest/config/development.toml index e1f94c4f80a3..358a591a6678 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -51,12 +51,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 = "" @@ -206,7 +200,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"} 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 index 19fbedccbbfe..40e65c149f26 100644 --- 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 @@ -1,3 +1,3 @@ -- Your SQL goes here ALTER TABLE business_profile -ADD COLUMN IF NOT EXISTS payment_link_config JSONB DEFAULT NULL; \ No newline at end of file +ADD COLUMN IF NOT EXISTS payment_link_config JSONB DEFAULT NULL; 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 index b48346c763ed..207fdc8817e1 100644 --- 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 @@ -1,2 +1,2 @@ -- Your SQL goes here -ALTER TABLE payment_link ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64) DEFAULT NULL; \ No newline at end of file +ALTER TABLE payment_link ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64) DEFAULT NULL; 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 index 2801a68c67ee..6af3e1e7f3df 100644 --- 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 @@ -1,2 +1,2 @@ -- This file should undo anything in `up.sql` -ALTER TABLE payment_intent DROP COLUMN IF EXISTS session_expiry; \ No newline at end of file +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 index f2ee81e847d8..e6ad0a728d44 100644 --- 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 @@ -1,2 +1,2 @@ -- Your SQL goes here -ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS session_expiry TIMESTAMP DEFAULT NULL; \ No newline at end of file +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS session_expiry TIMESTAMP DEFAULT NULL; 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-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-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/openapi/openapi_spec.json b/openapi/openapi_spec.json index bf0df4dc4b27..c50f687a1810 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -382,6 +382,117 @@ ] } }, + "/blocklist": { + "get": { + "tags": [ + "Blocklist" + ], + "operationId": "List Blocked fingerprints of a particular kind", + "parameters": [ + { + "name": "data_kind", + "in": "query", + "description": "Kind of the fingerprint list requested", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlocklistDataKind" + } + } + ], + "responses": { + "200": { + "description": "Blocked Fingerprints", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "Blocklist" + ], + "operationId": "Block a Fingerprint", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Fingerprint Blocked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "delete": { + "tags": [ + "Blocklist" + ], + "operationId": "Unblock a Fingerprint", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Fingerprint Unblocked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/customers": { "post": { "tags": [ @@ -473,15 +584,6 @@ "type": "string" } }, - { - "name": "customer_id", - "in": "path", - "description": "The unique identifier for the customer account", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "accepted_country", "in": "query", @@ -711,15 +813,6 @@ "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", "parameters": [ - { - "name": "customer_id", - "in": "path", - "description": "The unique identifier for the customer account", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "accepted_country", "in": "query", @@ -4053,6 +4146,95 @@ } ] }, + "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": { @@ -5556,6 +5738,8 @@ "payment_failed", "payment_processing", "payment_cancelled", + "payment_authorized", + "payment_captured", "action_required", "refund_succeeded", "refund_failed", @@ -6592,6 +6776,27 @@ } } }, + "ListBlocklistQuery": { + "type": "object", + "required": [ + "data_kind" + ], + "properties": { + "data_kind": { + "$ref": "#/components/schemas/BlocklistDataKind" + }, + "limit": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "offset": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + }, "MandateAmountData": { "type": "object", "required": [ @@ -8684,7 +8889,8 @@ "required": [ "theme", "logo", - "seller_name" + "seller_name", + "sdk_layout" ], "properties": { "theme": { @@ -8698,6 +8904,10 @@ "seller_name": { "type": "string", "description": "Custom merchant name for payment link" + }, + "sdk_layout": { + "type": "string", + "description": "Custom layout for sdk" } } }, @@ -8724,6 +8934,13 @@ "example": "hyperswitch", "nullable": true, "maxLength": 255 + }, + "sdk_layout": { + "type": "string", + "description": "Custom layout for sdk", + "example": "accordion", + "nullable": true, + "maxLength": 255 } } }, @@ -8760,8 +8977,8 @@ "PaymentLinkStatus": { "type": "string", "enum": [ - "Active", - "Expired" + "active", + "expired" ] }, "PaymentListConstraints": { @@ -10730,6 +10947,11 @@ }, "description": "List of incremental authorizations happened to the payment", "nullable": true + }, + "fingerprint": { + "type": "string", + "description": "Payment Fingerprint", + "nullable": true } } }, @@ -11678,6 +11900,12 @@ }, "profile_id": { "type": "string", + "description": "The id of business profile for this refund", + "nullable": true + }, + "merchant_connector_id": { + "type": "string", + "description": "The merchant_connector_id of the processor through which this payment went through", "nullable": true } }