Skip to content

feat(core): Add all_keys_required in Capture, Refund and RSync Flows#10178

Merged
likhinbopanna merged 15 commits intomainfrom
feat/all-keys-required-capture-refund
Nov 17, 2025
Merged

feat(core): Add all_keys_required in Capture, Refund and RSync Flows#10178
likhinbopanna merged 15 commits intomainfrom
feat/all-keys-required-capture-refund

Conversation

@Anurag-05-prog
Copy link
Contributor

@Anurag-05-prog Anurag-05-prog commented Nov 10, 2025

Type of Change

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

closes this issue

Description

Added all_keys_required in Capture, Refund and RSync Flows.

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?

Postman Tests

  1. Payments - Capture

Request:

curl --location 'http://localhost:8080/payments/pay_zGIAlSjMGujPpQdXF4ES/capture' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: ••••••' \
--data '
{
  "amount_to_capture": 1000,
  "currency": "USD",
  "all_keys_required": true
}'

Response:

{
    "payment_id": "pay_zGIAlSjMGujPpQdXF4ES",
    "merchant_id": "merchant_1763368771",
    "status": "succeeded",
    "amount": 1000,
    "net_amount": 1000,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 1000,
    "connector": "peachpayments",
    "client_secret": "pay_zGIAlSjMGujPpQdXF4ES_secret_X0njS1ZKwOZDpM4yuUAc",
    "created": "2025-11-17T10:57:30.574Z",
    "currency": "USD",
    "customer_id": "StripeCustomer",
    "customer": {
        "id": "StripeCustomer",
        "name": "John Doe",
        "email": "abcdef123@gmail.com",
        "phone": "999999999",
        "phone_country_code": "+65"
    },
    "description": "Its my first payment request",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "manual",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "6175",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "492303",
            "card_extended_bin": null,
            "card_exp_month": "01",
            "card_exp_year": "2028",
            "card_holder_name": "joseph Doe",
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "John",
            "last_name": "Doe",
            "origin_zip": null
        },
        "phone": null,
        "email": null
    },
    "billing": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "John",
            "last_name": "Doe",
            "origin_zip": null
        },
        "phone": null,
        "email": null
    },
    "order_details": null,
    "email": "abcdef123@gmail.com",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "https://duck.com/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": null,
    "manual_retry_allowed": null,
    "connector_transaction_id": "2aa0fc35-d182-4ec8-b3e8-96a7f37fcd9b",
    "frm_message": null,
    "metadata": {
        "udf1": "value1",
        "login_date": "2025-07-25T11:46:12Z",
        "new_customer": "true"
    },
    "connector_metadata": null,
    "feature_metadata": {
        "redirect_response": null,
        "search_tags": null,
        "apple_pay_recurring_details": null,
        "gateway_system": "direct"
    },
    "reference_id": "2aa0fc35-d182-4ec8-b3e8-96a7f37fcd9b",
    "payment_link": null,
    "profile_id": "pro_2CXBb8dKJbVIE6FdbhNs",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_6Wos8mYBczVrjlaw6VUC",
    "incremental_authorization_allowed": false,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-11-17T11:12:30.574Z",
    "fingerprint": null,
    "browser_info": {
        "language": "nl-NL",
        "time_zone": 0,
        "ip_address": "127.0.0.1",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
        "color_depth": 24,
        "java_enabled": true,
        "screen_width": 1536,
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "screen_height": 723,
        "java_script_enabled": true
    },
    "payment_channel": null,
    "payment_method_id": null,
    "network_transaction_id": null,
    "payment_method_status": null,
    "updated": "2025-11-17T10:57:37.745Z",
    "split_payments": null,
    "frm_metadata": null,
    "extended_authorization_applied": null,
    "extended_authorization_last_applied_at": null,
    "request_extended_authorization": null,
    "capture_before": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null,
    "card_discovery": "manual",
    "force_3ds_challenge": false,
    "force_3ds_challenge_trigger": false,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "is_iframe_redirection_enabled": null,
    "whole_connector_response": "{\"transactionId\":\"2aa0fc35-d182-4ec8-b3e8-96a7f37fcd9b\",\"referenceId\":\"pay_zGIAlSjMGujPpQdXF4ES_1\",\"transactionResult\":\"approved_confirmed\",\"cardNotPresentRefundableStatus\":\"not_refundable\",\"responseCode\":{\"value\":\"00\",\"description\":\"Approved or completed successfully\",\"isoCodeDescription\":\"Approved or completed successfully\",\"terminalOutcomeString\":\"Approved\",\"receiptString\":\"Approved\",\"explanation\":\"Transaction approved by acquirer or issuer. Other factors may impact the final outcome of the transaction.\"},\"ecommerceCardPaymentOnlyTransactionData\":{\"rrn\":\"***\",\"stan\":\"***\",\"approvalCode\":\"***\",\"settlementDate\":\"2025-11-17\",\"amount\":{\"amount\":1000,\"currencyCode\":\"ZAR\",\"displayValue\":\"R10.00\"},\"card\":{\"maskedPan\":\"492303**********\",\"binNumber\":\"492303\",\"scheme\":\"Visa\",\"cardholderName\":\"joseph Doe\",\"expiryYear\":\"28\",\"expiryMonth\":\"01\",\"productType\":\"CREDIT\",\"bankName\":\"NEDB\",\"countryCode\":\"ZA\"}}}",
    "enable_partial_authorization": null,
    "enable_overcapture": null,
    "is_overcapture_enabled": null,
    "network_details": null,
    "is_stored_credential": null,
    "mit_category": null,
    "billing_descriptor": null
}
  1. Refunds - Create

Request:

curl --location 'http://localhost:8080/refunds' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: ••••••' \
--data '{
    "payment_id": "pay_zGIAlSjMGujPpQdXF4ES",
    "amount": 1000,
    "reason": "Customer returned product",
    "refund_type": "instant",
    "all_keys_required": true,
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    } 
}'

Response:

{
    "refund_id": "ref_GUEtZKp01hBpqklxFt4x",
    "payment_id": "pay_zGIAlSjMGujPpQdXF4ES",
    "amount": 1000,
    "currency": "USD",
    "status": "succeeded",
    "reason": "Customer returned product",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    },
    "error_message": null,
    "error_code": null,
    "unified_code": null,
    "unified_message": null,
    "created_at": "2025-11-17T10:58:16.781Z",
    "updated_at": "2025-11-17T10:58:17.573Z",
    "connector": "peachpayments",
    "profile_id": "pro_2CXBb8dKJbVIE6FdbhNs",
    "merchant_connector_id": "mca_6Wos8mYBczVrjlaw6VUC",
    "split_refunds": null,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "raw_connector_response": "{\"transactionId\":\"dd637cd1-4987-4653-a3e7-af85b9909f4b\",\"originalTransactionId\":\"2aa0fc35-d182-4ec8-b3e8-96a7f37fcd9b\",\"referenceId\":\"ref_GUEtZKp01hBpqklxFt4x\",\"transactionResult\":\"approved_confirmed\",\"cardNotPresentRefundableStatus\":\"not_refundable\",\"transactionType\":{\"value\":20,\"description\":\"Refund\"},\"responseCode\":{\"value\":\"00\",\"description\":\"Approved or completed successfully\",\"terminalOutcomeString\":\"Approved\",\"receiptString\":\"Approved\"},\"ecommerceCardPaymentOnlyTransactionData\":{\"rrn\":\"*****\",\"stan\":\"****\",\"approvalCode\":\"****\",\"settlementDate\":\"2025-11-17\",\"amount\":{\"amount\":1000,\"currencyCode\":\"ZAR\",\"displayValue\":\"R10.00\"},\"card\":{\"maskedPan\":\"492303**********\",\"binNumber\":\"492303\",\"scheme\":\"Visa\",\"cardholderName\":\"joseph Doe\",\"expiryYear\":\"28\",\"expiryMonth\":\"01\",\"productType\":\"CREDIT\",\"bankName\":\"NEDB\",\"countryCode\":\"ZA\"}},\"refundBalanceData\":{\"amount\":{\"amount\":1000,\"currencyCode\":\"ZAR\",\"displayValue\":\"R10.00\"},\"balance\":{\"amount\":0,\"currencyCode\":\"ZAR\",\"displayValue\":\"R0.00\"},\"refundHistory\":[{\"transactionId\":\"dd637cd1-4987-4653-a3e7-af85b9909f4b\",\"referenceId\":\"ref_GUEtZKp01hBpqklxFt4x\",\"amount\":{\"amount\":1000,\"currencyCode\":\"ZAR\",\"displayValue\":\"R10.00\"},\"transactionTime\":\"2025-11-17T10:58:17.012157224Z\"}]},\"transactionTime\":\"2025-11-17T10:58:17.012157224Z\",\"paymentMethod\":\"ecommerce_card_payment_only\",\"voidableUntilTime\":\"2025-11-17T22:00:00Z\",\"posData\":{\"exiAuthRef\":\"API-KEY: #############\"}}"
}
  1. Refunds - Retrieve

Request:

curl --location 'http://localhost:8080/refunds/ref_lhovmLN57IY4XtmaN2CS?all_keys_required=true' \
--header 'Accept: application/json' \
--header 'api-key: {{api_key}}'

Response:

{
    "refund_id": "ref_lhovmLN57IY4XtmaN2CS",
    "payment_id": "pay_xDivZ2j0BDcLVLFuJZIR",
    "amount": 1000,
    "currency": "USD",
    "status": "succeeded",
    "reason": "Customer returned product",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    },
    "error_message": null,
    "error_code": null,
    "unified_code": null,
    "unified_message": null,
    "created_at": "2025-11-17T08:41:51.768Z",
    "updated_at": "2025-11-17T10:58:49.255Z",
    "connector": "peachpayments",
    "profile_id": "pro_2CXBb8dKJbVIE6FdbhNs",
    "merchant_connector_id": "mca_6Wos8mYBczVrjlaw6VUC",
    "split_refunds": null,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "raw_connector_response": "{\"transactionId\":\"dd2acbb1-6844-44ef-a7de-fec7d3a85fb0\",\"originalTransactionId\":\"22993ed9-88b6-417e-8203-9118b7b4b962\",\"referenceId\":\"ref_lhovmLN57IY4XtmaN2CS\",\"transactionResult\":\"approved_confirmed\",\"cardNotPresentRefundableStatus\":\"not_refundable\",\"transactionType\":{\"value\":20,\"description\":\"Refund\"},\"responseCode\":{\"value\":\"00\",\"description\":\"Approved or completed successfully\",\"isoCodeDescription\":\"Approved or completed successfully\",\"terminalOutcomeString\":\"Approved\",\"receiptString\":\"Approved\",\"explanation\":\"Transaction approved by acquirer or issuer. Other factors may impact the final outcome of the transaction.\"},\"ecommerceCardPaymentOnlyTransactionData\":{\"rrn\":\"*****\",\"stan\":\"****\",\"settlementDate\":\"2025-11-17\",\"amount\":{\"amount\":1000,\"currencyCode\":\"ZAR\",\"displayValue\":\"R10.00\"},\"card\":{\"maskedPan\":\"492303**********\",\"binNumber\":\"492303\",\"scheme\":\"Visa\",\"cardholderName\":\"\",\"expiryYear\":\"28\",\"expiryMonth\":\"01\",\"productType\":\"CREDIT\",\"bankName\":\"NEDB\",\"countryCode\":\"ZA\"}},\"voidableUntilTime\":\"2025-11-17T22:00:00Z\",\"transactionTime\":\"2025-11-17T08:41:52.070342Z\",\"paymentMethod\":\"ecommerce_card_payment_only\"}"
}

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

@Anurag-05-prog Anurag-05-prog requested review from a team as code owners November 10, 2025 06:36
@semanticdiff-com
Copy link

semanticdiff-com bot commented Nov 10, 2025

@Anurag-05-prog Anurag-05-prog linked an issue Nov 10, 2025 that may be closed by this pull request
2 tasks
@hyperswitch-bot hyperswitch-bot bot added the M-api-contract-changes Metadata: This PR involves API contract changes label Nov 10, 2025
Anurag Singh added 2 commits November 10, 2025 12:21
…spay/hyperswitch into feat/all-keys-required-capture-refund
@codecov
Copy link

codecov bot commented Nov 10, 2025

Codecov Report

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

Files with missing lines Patch % Lines
crates/router/src/core/refunds_v2.rs 0.00% 12 Missing ⚠️
crates/router/src/services/api.rs 0.00% 3 Missing ⚠️
...tes/router/src/core/payments/flows/capture_flow.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #10178   +/-   ##
=======================================
  Coverage        ?    6.50%           
=======================================
  Files           ?     1232           
  Lines           ?   307877           
  Branches        ?        0           
=======================================
  Hits            ?    20041           
  Misses          ?   287836           
  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.

@Anurag-05-prog Anurag-05-prog self-assigned this Nov 10, 2025
match (
connector_status,
attempt_status,
payment_data.frm_message.to_owned(),
Copy link
Contributor

Choose a reason for hiding this comment

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

no need of nested match cases here. we can directly have payment_data.frm_message.frm_status.to_owned() here, and for Fraud and ManualReview return attempt_status

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These nested match cases were already there. I just introduced an extra check on PSync flow that if a payment is already in terminal status then the status should not be updated after PSync call (find more context here: #9925)

Copy link
Contributor

Choose a reason for hiding this comment

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

anyways you can make the changes nits mainly, same code block is repeating twice

Copy link
Contributor Author

@Anurag-05-prog Anurag-05-prog Nov 11, 2025

Choose a reason for hiding this comment

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

We can’t directly use payment_data.frm_message.frm_status.to_owned() since frm_message is an Option type.
Therefore, this block of code will need to be repeated in both cases:

router_data.get_attempt_status_for_db_update(
    &payment_data,
    router_data.amount_captured,
    router_data
        .minor_amount_capturable
        .map(MinorUnit::get_amount_as_i64),
)?,

};

Ok(refund.foreign_into())
let mut refund_response: api::RefundResponse = refund.foreign_into();
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of having a mutable variable, can we use the foreign_from for api::RefundResponse?
which is already there at 1636

let flow_name = core_utils::get_flow_name::<F>()?;
let updated_attempt_status =
if flow_name == "PSync" && attempt_status.is_terminal_status() {
// For PSync flows, if payment is already in terminal state, don't update the status
Copy link
Contributor

Choose a reason for hiding this comment

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

For PSync flow, if the attempt status is terminal, we will not call the connector at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hrithikesh026, If all_keys_required is set to true, we call the connector to retrieve the raw connector response regardless of the payment status. This is necessary because raw_connector_response is maintained as a stateless field in Hyperswitch. You can find more context here: #9925

Copy link
Contributor

Choose a reason for hiding this comment

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

attempt status should directly map with connector status right. If the connector status changes from terminal status, we should also change the attempt status.

merchant_context,
refund,
router_data.clone(),
None,
Copy link
Contributor

Choose a reason for hiding this comment

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

this should not be none

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are currently not focusing on UCS shadow flow for whole_connector_response as UCS itself supports this feature.

Copy link
Contributor

Choose a reason for hiding this comment

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

here we first do direct HS call and then shadow with UCS. Your feature won't work when shadow mode is enabled if this is hardcoded to none

// Check if current status is terminal and preserve it in PSync flows
let flow_name = core_utils::get_flow_name::<F>()?;
let updated_attempt_status =
if flow_name == "PSync" && attempt_status.is_terminal_status() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please create a discussion on why this logic is needed.

hrithikesh026
hrithikesh026 previously approved these changes Nov 14, 2025
pub issuer_error_message: Option<String>,
/// Contains whole connector response
#[schema(value_type = Option<String>)]
pub whole_connector_response: Option<masking::Secret<String>>,
Copy link
Member

Choose a reason for hiding this comment

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

what is reason we have a different keep name here?

Suggested change
pub whole_connector_response: Option<masking::Secret<String>>,
pub raw_connector_response: Option<masking::Secret<String>>,

)?,
};
// Check if current status is terminal and preserve it in PSync flows
let flow_name = core_utils::get_flow_name::<F>()?;
Copy link
Member

Choose a reason for hiding this comment

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

This kind of overloading is wrong, we should not do this in this way, why this logic is required?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverting this change as it'll require a separate discussion. Will take this in a separate PR when the changes will be fully discussed.

jarnura
jarnura previously approved these changes Nov 17, 2025
@likhinbopanna likhinbopanna added this pull request to the merge queue Nov 17, 2025
Merged via the queue into main with commit 6f582dc Nov 17, 2025
27 of 31 checks passed
@likhinbopanna likhinbopanna deleted the feat/all-keys-required-capture-refund branch November 17, 2025 15:31
Anurag-05-prog added a commit that referenced this pull request Nov 17, 2025
…10178)

Co-authored-by: Anurag Singh <anurag.singh.001@MacBookPro.lan>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Anurag Singh <anurag.singh.001@Anurag-Singh-WPMHJ5619X.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

M-api-contract-changes Metadata: This PR involves API contract changes

Projects

None yet

5 participants