Skip to content

Conversation

@sagarnaikjuspay
Copy link
Contributor

@sagarnaikjuspay sagarnaikjuspay commented Dec 2, 2025

…onse missing value fixes

Type of Change

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

Description

In revenue recovery get intent and get recovery payment list had some missing values due to missing active attempt id in payment intent which is not able to get the latest attempt even though payment having a payment attempt in the db. so this pr will fix that issues and related data is been populated in the response even though there's a missing active attempt id store in the payment intent

API Response Improvements

  • The RevenueRecoveryGetIntentResponse struct now uses PaymentAmountDetailsResponse for the amount_details field, allowing it to represent both intent-level and attempt-level amount details more accurately.
        "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": 0
            },
  • Added a new created_at field to RevenueRecoveryGetIntentResponse to expose the payment creation timestamp in API responses.
"created": "2025-12-02T12:24:20.076Z",

Payment Attempt Selection Logic

  • Updated the logic in revenue_recovery_get_intent_core and revenue_recovery_list_payments to fetch the most relevant payment attempt for each payment intent. The selection prioritizes the active attempt if available, otherwise the most recent attempt is chosen. This ensures downstream consumers receive the correct attempt data.
  • The selected payment attempt is now passed to the response transformer, enabling amount details to reflect the correct attempt or intent context.
    Transformer Signature Update
  • The generate_revenue_recovery_get_intent_response function signature has been updated to accept an optional payment_attempt, supporting the improved amount details logic and new fields.

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?

Get recovery list payments api

curl --location 'http://localhost:8080/v2/payments/recovery-list' \
--header 'Content-Type: application/json' \
--header 'x-profile-id: ******' \
--header 'Authorization: api-key=******'

response

{
    "count": 1,
    "total_count": 1,
    "data": [
        {
            "id": "12345_pay_019adf05556570019fa6d4ce294716b6",
            "merchant_id": "cloth_seller_FNV6DwhshGLsuQLgGQfM",
            "profile_id": "pro_0uwqqjOjujEd9pIlwPaJ",
            "customer_id": null,
            "status": "scheduled",
            "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": 0
            },
            "created": "2025-12-02T12:24:20.076Z",
            "payment_method_type": "card",
            "payment_method_subtype": "credit",
            "connector": "worldpayvantiv",
            "merchant_connector_id": "mca_VAT2dYqYmtgURuqDz42a",
            "customer": null,
            "merchant_reference_id": "12453444456762",
            "description": null,
            "attempt_count": 0,
            "error": {
                "code": "No error code",
                "message": "PaymentStatusNotFound",
                "reason": "PaymentStatusNotFound",
                "unified_code": null,
                "unified_message": null,
                "network_advice_code": null,
                "network_decline_code": null,
                "network_error_message": null
            },
            "cancellation_reason": null,
            "modified_at": "2025-12-02T12:24:22.201Z",
            "last_attempt_at": "2024-05-29T08:10:58.000Z"
        }
    ]
}

Get revenue recovery intent

curl --location 'http://localhost:8080/v2/payments/12345_pay_019adec8a8177b70ba9d7916792da9f5/get-revenue-recovery-intent' \
--header 'x-profile-id: *******' \
--header 'Authorization: api-key=*******' \
--data ''

response

{
    "id": "12345_pay_019adec8a8177b70ba9d7916792da9f5",
    "status": "processing",
    "amount_details": {
        "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": 2250,
        "amount_captured": 2250
    },
    "client_secret": null,
    "profile_id": "pro_0uwqqjOjujEd9pIlwPaJ",
    "merchant_reference_id": "12453444456762",
    "routing_algorithm_id": null,
    "capture_method": "automatic",
    "authentication_type": "no_three_ds",
    "billing": {
        "address": {
            "city": null,
            "country": "US",
            "line1": null,
            "line2": null,
            "line3": null,
            "zip": null,
            "state": "CA",
            "first_name": null,
            "last_name": null,
            "origin_zip": null
        },
        "phone": null,
        "email": null
    },
    "shipping": null,
    "customer_id": null,
    "customer_present": "present",
    "description": null,
    "return_url": null,
    "setup_future_usage": "off_session",
    "apply_mit_exemption": "Skip",
    "statement_descriptor": null,
    "order_details": null,
    "allowed_payment_method_types": null,
    "metadata": null,
    "connector_metadata": 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_6GovbAMzXHjCpkmnGUsE",
            "active_attempt_payment_connector_id": "mca_VAT2dYqYmtgURuqDz42a",
            "billing_connector_payment_details": {
                "payment_processor_token": "271349160840009",
                "connector_customer_id": "853818"
            },
            "payment_method_type": "card",
            "payment_method_subtype": "credit",
            "connector": "worldpayvantiv",
            "billing_connector_payment_method_details": {
                "type": "card",
                "value": {
                    "card_network": "AmericanExpress",
                    "card_issuer": "Wells Fargo"
                }
            },
            "invoice_next_billing_time": null,
            "invoice_billing_started_at_time": null,
            "first_payment_attempt_pg_error_code": "No error code",
            "first_payment_attempt_network_decline_code": null,
            "first_payment_attempt_network_advice_code": null
        }
    },
    "payment_link_enabled": "Skip",
    "payment_link_config": null,
    "request_incremental_authorization": "false",
    "split_txns_enabled": "skip",
    "expires_on": "2025-12-02T11:33:03.544Z",
    "created_at": "2025-12-02T11:18:03.544Z",
    "frm_metadata": null,
    "request_external_three_ds_authentication": "Skip",
    "enable_partial_authorization": null,
    "card_attached": 3
}

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

@sagarnaikjuspay sagarnaikjuspay self-assigned this Dec 2, 2025
@sagarnaikjuspay sagarnaikjuspay requested review from a team as code owners December 2, 2025 12:56
@sagarnaikjuspay sagarnaikjuspay added the S-waiting-on-review Status: This PR has been implemented and needs to be reviewed label Dec 2, 2025
@semanticdiff-com
Copy link

semanticdiff-com bot commented Dec 2, 2025

Review changes with  SemanticDiff

Changed Files
File Status
  crates/router/src/core/payments/transformers.rs  64% smaller
  crates/router/src/core/payments.rs  2% smaller
  crates/api_models/src/payments.rs  1% smaller

@codecov
Copy link

codecov bot commented Dec 2, 2025

Codecov Report

❌ Patch coverage is 0% with 73 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@1c9e9a4). Learn more about missing BASE report.

Files with missing lines Patch % Lines
crates/router/src/core/payments.rs 0.00% 66 Missing ⚠️
crates/router/src/core/payments/transformers.rs 0.00% 7 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #10508   +/-   ##
=======================================
  Coverage        ?    6.47%           
=======================================
  Files           ?     1251           
  Lines           ?   311581           
  Branches        ?        0           
=======================================
  Hits            ?    20165           
  Misses          ?   291416           
  Partials        ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

pub expires_on: PrimitiveDateTime,

/// Time when the payment was created
#[serde(with = "common_utils::custom_serde::iso8601")]
Copy link
Contributor

Choose a reason for hiding this comment

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

generate openapi spec for the new introduced fields?

Copy link
Contributor

Choose a reason for hiding this comment

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

Please generate openapi spec

.and_then(|active_attempt_id| {
attempts
.iter()
.find(|attempt| attempt.id == *active_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.

why would active_attempt_id not be populated in intent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

when the payment enters revenue recovery from an external source and the attempts are marked as TriggeredBy::External so in this case active_attempt_id is set to None cause they're the external attempts and not managed by hyperswitch so active attempt is not tracked. When hyperswitch revenuer recovery creates a internal retry attempt ,in this case active attempt id is set to 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.

don't list payment attempt from DB and filter it. Find a way to point query the appropriate attempt from DB.

Like if active_attempt_id is present, fetch attempt using that id.
If not present, fetch only the latest attempt for the intent_id

let attempt_futures: Vec<_> = list
.iter()
.map(|(payment_intent, payment_attempt)| {
let platform_clone = platform.clone();
Copy link
Contributor

@hrithikesh026 hrithikesh026 Dec 3, 2025

Choose a reason for hiding this comment

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

Suggested change
let platform_clone = platform.clone();
let platform_ref = &platform;

Copy link
Contributor

Choose a reason for hiding this comment

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

If an intent has an active attempt, its id should be populated in active_attempt_id of intent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Please make this change https://github.com/juspay/hyperswitch/pull/10508/files#r2584064017

we should not clone any object. instead use reference wherever possible

Comment on lines +8498 to +8520
db.find_payment_attempts_by_payment_intent_id(
&payment_intent.id.clone(),
platform_clone.get_processor().get_key_store(),
platform_clone.get_processor().get_account().storage_scheme,
)
.await
.ok()
.and_then(|attempts| {
payment_intent
.active_attempt_id
.as_ref()
.and_then(|active_attempt_id| {
attempts
.iter()
.find(|attempt| attempt.id == *active_attempt_id)
.cloned()
})
.or_else(|| {
attempts
.iter()
.max_by_key(|attempt| attempt.created_at)
.cloned()
})
Copy link
Contributor

Choose a reason for hiding this comment

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

don't list payment attempt from DB and filter it. Find a way to point query the appropriate attempt from DB.

Like if active_attempt_id is present, fetch attempt using that id.
If not present, fetch only the latest attempt for the intent_id

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: This PR has been implemented and needs to be reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: get recovery list and get recovery intent response missing values fix

3 participants