Skip to content

feat(webhooks): Provide outgoing webhook support for revenue recovery#9294

Merged
likhinbopanna merged 42 commits intomainfrom
provide_outgoing_webhook_support_for_revenue_recovery
Sep 11, 2025
Merged

feat(webhooks): Provide outgoing webhook support for revenue recovery#9294
likhinbopanna merged 42 commits intomainfrom
provide_outgoing_webhook_support_for_revenue_recovery

Conversation

@NISHANTH1221
Copy link
Contributor

@NISHANTH1221 NISHANTH1221 commented Sep 7, 2025

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

This PR enables outgoing webhook support for revenue recovery.

When the retries get succeeded that are done using the revenue recovery system, an outgoing webhook needs to be triggered to the url in business profile. This pr adds that support.

Additional Changes

  • This PR modifies the API contract
  • This PR modifies the database schema
  • This PR modifies application configuration/environment variables

Motivation and Context

How did you test it?

  1. create a billing profile with webhook.site url to monitor the webhooks.
  2. Create an payment mca using this
curl --location 'http://localhost:8080/v2/connector-accounts' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'x-merchant-id:  {{merchant-id}}'
--header 'x-profile-id:  {{profile-id}}' \
--header 'Authorization: {{api-key}}'
--header 'api-key:{{api-key}}' \
--data '{
    "connector_type": "payment_processor",
    "connector_name": "stripe",
    "connector_account_details": {
        "auth_type": "HeaderKey",
        "api_key": "{{api_key}}"
    },
    "payment_methods_enabled": [
        {
            "payment_method_type": "card",
            "payment_method_subtypes": [
                {
                    "payment_method_subtype": "credit",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": -1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                },
                {
                    "payment_method_subtype": "debit",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": -1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                },
                 {
                    "payment_method_subtype": "card",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": -1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        }
    ],
    "frm_configs": null,
    "connector_webhook_details": {
        "merchant_secret": ""
    },
    "metadata": {
        "report_group": "Hello",
        "merchant_config_currency": "USD"
    },
    "profile_id": "{{profile_id}}"
}'
  1. create an billing mca

curl --location 'http://localhost:8080/v2/connector-accounts' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'x-merchant-id:  {{merchant-id}}'
--header 'x-profile-id:  {{profile-id}}' \
--header 'Authorization: {{api-key}}'
--header 'api-key:{{api-key}}' \
--data '{
    "connector_type": "billing_processor",
    "connector_name": "custombilling",
    "connector_account_details": {
        "auth_type": "NoKey"
    },
    "feature_metadata" : {
        "revenue_recovery" : {
            "max_retry_count" : 9,
            "billing_connector_retry_threshold" : 0,
            "billing_account_reference" :{
                "{{payment_mca}}" : "{{payment_mca}}"
            },
            "switch_payment_method_config" : {
                "retry_threshold" : 0,
                "time_threshold_after_creation": 0
            }
        }
    },
    "profile_id": "{{profile_id}}"
}'
  1. switch revenue recovery config to cascading
curl --location --request PUT 'http://localhost:8080/v2/profiles/pro_DD6ZRORqBtEwR3ITjseM' \
--header 'x-merchant-id: {{erchant-id}}' \
--header 'Authorization:  {{api-key}}' \
--header 'Content-Type: application/json' \
--header 'api-key:  {{api-key}}' \
--data '{
    "revenue_recovery_retry_algorithm_type": "cascading"
}   '
  1. Go to stripe dashboard and create a customer and attach a card for that customer
  2. Hit this recovery api with that customer_id and payment_method_id
curl --location 'http://localhost:8080/v2/payments/recovery' \
--header 'Authorization: {{api-key}}'
--header 'x-profile-id: {{profile-id}}
--header 'x-merchant-id: {{merchant-id}}'
--header 'Content-Type: application/json' \
--header 'api-key:{{api_key}}' \
--data '{
    "amount_details": {
      "order_amount": 2250,
      "currency": "USD"
    },
    "merchant_reference_id": "test_ffhgfewf3476f5",
    "connector_transaction_id": "1831984",
    "connector_customer_id": "{{customer_id_from_stripe}}",
    "error": {
      "code": "110",
      "message": "Insufficient Funds"
    },
    "billing": {
      "address": {
        "state": "CA",
        "country": "US"
      }
    },
    "payment_method_type": "card",
    "payment_method_sub_type": "credit",
    "payment_method_data": {
      "primary_processor_payment_method_token": "{{payment_method_id_from_stripe}}",
      "additional_payment_method_info": {
        "card_exp_month": "12",
        "card_exp_year": "25",
        "last_four_digits": 1997,
        "card_network": "Visa",
        "card_issuer": "Wells Fargo NA",
        "card_type": "credit"
      }
    },
    "billing_started_at": "2024-05-29T08:10:58Z",
    "transaction_created_at": "2024-05-29T08:10:58Z",
    "attempt_status": "failure",
    "action": "schedule_failed_payment",
    "billing_merchant_connector_id": "{{billing_mca}}
    "payment_merchant_connector_id": "{{payment_mca}}"
  }'
  1. There will be a process tracker entry now and wait till it get triggered. Based on the test card if we mentioned a successful card's payment method id then we will get a webhook to webhook.site url.
    Sample screen shot and sample webhook body are mentioned below
Screenshot 2025-09-08 at 1 55 58 AM Sample body of the webhook
{
  "merchant_id": "cloth_seller_I7nS4iwZnxRDJtGfiNTy",
  "event_id": "evt_019925c838c877c282d3eaf5b86e9036",
  "event_type": "payment_succeeded",
  "content": {
    "type": "payment_details",
    "object": {
      "id": "12345_pay_019925c5959a79418301a9460b92edb1",
      "status": "succeeded",
      "amount": {
        "order_amount": 2250,
        "currency": "USD",
        "shipping_cost": null,
        "order_tax_amount": null,
        "external_tax_calculation": "skip",
        "surcharge_calculation": "skip",
        "surcharge_amount": null,
        "tax_on_surcharge": null,
        "net_amount": 2250,
        "amount_to_capture": null,
        "amount_capturable": 0,
        "amount_captured": 2250
      },
      "customer_id": null,
      "connector": "stripe",
      "created": "2025-09-07T20:02:09.947Z",
      "modified_at": "2025-09-07T20:05:02.777Z",
      "payment_method_data": {
        "billing": null
      },
      "payment_method_type": "card",
      "payment_method_subtype": "credit",
      "connector_transaction_id": "pi_3S4opd2KXBHSNjod0tD34Dmw",
      "connector_reference_id": "pi_3S4opd2KXBHSNjod0tD34Dmw",
      "merchant_connector_id": "mca_MbQwWzi4tFItmgYAmshC",
      "browser_info": null,
      "error": null,
      "shipping": null,
      "billing": null,
      "attempts": null,
      "connector_token_details": {
        "token": "pm_1S4olu2KXBHSNjodBbiedqY9",
        "connector_token_request_reference_id": null
      },
      "payment_method_id": null,
      "next_action": null,
      "return_url": "https://google.com/success",
      "authentication_type": "no_three_ds",
      "authentication_type_applied": "no_three_ds",
      "is_iframe_redirection_enabled": null,
      "merchant_reference_id": "test_ffhgfewf3476f5",
      "raw_connector_response": null,
      "feature_metadata": {
        "redirect_response": null,
        "search_tags": null,
        "apple_pay_recurring_details": null,
        "revenue_recovery": {
          "total_retry_count": 2,
          "payment_connector_transmission": "ConnectorCallSucceeded",
          "billing_connector_id": "mca_ppEVMjRWgTiGyCiwFtg9",
          "active_attempt_payment_connector_id": "mca_MbQwWzi4tFItmgYAmshC",
          "billing_connector_payment_details": {
            "payment_processor_token": "pm_1S4olu2KXBHSNjodBbiedqY9",
            "connector_customer_id": "cus_T0qSE723C5Xxic"
          },
          "payment_method_type": "card",
          "payment_method_subtype": "credit",
          "connector": "stripe",
          "billing_connector_payment_method_details": {
            "type": "card",
            "value": {
              "card_network": "Visa",
              "card_issuer": "Wells Fargo NA"
            }
          },
          "invoice_next_billing_time": null,
          "invoice_billing_started_at_time": null,
          "first_payment_attempt_pg_error_code": "resource_missing",
          "first_payment_attempt_network_decline_code": null,
          "first_payment_attempt_network_advice_code": null
        }
      }
    }
  },
  "timestamp": "2025-09-07T20:05:02.792Z"
}

Checklist

  • I formatted the code cargo +nightly fmt --all
  • I addressed lints thrown by cargo clippy
  • I reviewed the submitted code
  • I added unit tests for my changes where possible

@NISHANTH1221 NISHANTH1221 self-assigned this Sep 7, 2025
@NISHANTH1221 NISHANTH1221 requested review from a team as code owners September 7, 2025 20:28
@NISHANTH1221 NISHANTH1221 added A-webhooks Area: Webhook flows api-v2 labels Sep 7, 2025
@hyperswitch-bot hyperswitch-bot bot added the M-api-contract-changes Metadata: This PR involves API contract changes label Sep 7, 2025
@NISHANTH1221 NISHANTH1221 requested a review from a team as a code owner September 9, 2025 08:09
@NISHANTH1221 NISHANTH1221 requested a review from a team as a code owner September 10, 2025 14:07
// External Payments which enter the calculate workflow for the first time will have active attempt id as None
// Then we dont need to send an webhook to the merchant as its not a failure from our side.
// Thus we dont need to a payment get call for such payments.
let is_there_an_active_payment_attempt_id = payment_intent.active_attempt_id.is_some();
Copy link
Contributor

@Aprabhat19 Aprabhat19 Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not handle is there an active attempt id like this, do a map and handle the None case if required

merchant_context: &domain::MerchantContext,
is_there_an_active_payment_attempt_id: bool,
) -> Result<Option<ApplicationResponse<PaymentsResponse>>, sch_errors::ProcessTrackerError> {
if is_there_an_active_payment_attempt_id {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use if else here, use map , and instead of passing a bool flag around, get the active attempt from the revenue_recovery_payment_data, or pass an optional id here, and then perform the check here for the presence of an active attempt id

if is_there_an_active_payment_attempt_id {
let operation = payments::operations::PaymentGet;

let request = router_api_types::PaymentsRetrieveRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is already a function call psync api, try using that, all the variable fields pass it as an argument to that function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that would call the connector(force_sync is true in the request) to get the details I need just the latest attempt and intent status data

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can pass force_sync as an argument

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just need get tracker response

state: &SessionState,
payment_intent: &PaymentIntent,
revenue_recovery_payment_data: &pcr::RevenueRecoveryPaymentData,
mut revenue_recovery_metadata: api_models::payments::PaymentRevenueRecoveryMetadata,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not pass a mutable argument to the function , make it mutable when you would want to use it . Passing a mutable reference along is not a good practice

mut revenue_recovery_metadata: api_models::payments::PaymentRevenueRecoveryMetadata,
is_there_an_active_payment_attempt_id: bool,
) -> Result<Option<()>, sch_errors::ProcessTrackerError> {
if is_there_an_active_payment_attempt_id {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid using if else , and preferably do not pass this as an argument

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed in future to not send outgoing webhook when a intent moves to calculate to the first time

revenue_recovery_payment_data: &storage::revenue_recovery::RevenueRecoveryPaymentData,
payment_attempt: PaymentAttempt,
revenue_recovery_metadata: &mut PaymentRevenueRecoveryMetadata,
psync_response: &PaymentStatusData<router_flow_types::PSync>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this pass it in the session revenue_recovery_payment_data

hrithikesh026
hrithikesh026 previously approved these changes Sep 11, 2025
let profile_id = revenue_recovery_payment_data.profile.get_id();
let billing_mca_id = revenue_recovery_payment_data.billing_mca.get_id();

let event_class = common_enums::EventClass::Payments;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this variable is not necessary.

srujanchikke
srujanchikke previously approved these changes Sep 11, 2025
swangi-kumari
swangi-kumari previously approved these changes Sep 11, 2025
@likhinbopanna likhinbopanna added this pull request to the merge queue Sep 11, 2025
Merged via the queue into main with commit 2db6153 Sep 11, 2025
21 of 25 checks passed
@likhinbopanna likhinbopanna deleted the provide_outgoing_webhook_support_for_revenue_recovery branch September 11, 2025 17:13
pixincreate added a commit that referenced this pull request Sep 11, 2025
…ee-ds

* 'main' of github.com:juspay/hyperswitch:
  feat(webhooks): Provide outgoing webhook support for revenue recovery (#9294)
  feat(connector): Add Peachpayments Template Code (#9363)
  feat(connector): [Paysafe] Implement card 3ds flow (#9305)
  feat(router): Add Connector changes for 3ds (v2) (#9117)
  feat(connector): [ADYEN] Add support to ideal Mandate Webhook (#9347)
  refactor(core): accept manual retry from profile  (#9302)
  fix(nuvei): nuvei 3ds fix + psync fix (#9279)
  fix(connector): [checkout] Add US Support for Apple Pay and Google Pay + Enhanced Checkout Response Data (#9356)
  fix(router): adding connector_customer_id for external vault proxy (#9263)
  feat(core): Add first_name and last_name as Secret<String> Types.  (#9326)
  feat(injector): injector request formation changes (#9306)
  fix(revenue-recovery): Update Redis TTL for customer locks after token selection (#9282)
  chore(version): 2025.09.11.0
  refactor(connector): [Paysafe] fix wasm (#9349)
  refactor(connector): rename RevenueRecoveryRecordBack as InvoiceRecordBack (#9321)
  feat(connector): [checkout] add support for MOTO payments (#9327)
  feat(connector): enhance ACI connector with comprehensive 3DS support - DRAFT (#8986)
  feat(core): [Retry] MIT Retries (#8628)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-webhooks Area: Webhook flows api-v2 M-api-contract-changes Metadata: This PR involves API contract changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(webhooks): Provide outgoing webhook support for revenue recovery

7 participants