Skip to content

fix(core): allow Authorize without payment_method on the 3DS return leg - #2126

Closed
shuklatushar226 wants to merge 1 commit into
mainfrom
fix/authorize-3ds-completion-without-payment-method
Closed

fix(core): allow Authorize without payment_method on the 3DS return leg#2126
shuklatushar226 wants to merge 1 commit into
mainfrom
fix/authorize-3ds-completion-without-payment-method

Conversation

@shuklatushar226

Copy link
Copy Markdown
Contributor

Problem

PaymentService::authorize required payment_method unconditionally:

proto_payload.payment_method.clone().ok_or(MissingRequiredField { field_name: "payment_method" })?

Connectors that finalise 3-D Secure through the Authorize flow — Ilixium, NMI, Airwallex,
Nexixpay and others that key off request.redirect_response.is_some() — are re-invoked after
the ACS posts back to TermUrl. At that point the caller no longer holds the raw instrument, so
Hyperswitch sends payment_method_data: None and omits the proto field (it is optional in both
the proto and Hyperswitch's CompleteAuthorizeData).

UCS therefore rejected the completion call with InvalidArgument: Missing required field: payment_method before any connector code ran, making 3DS completion impossible for every
connector that uses this pattern.

Fix

payment_method is now required only when the call is not a redirect-return leg. The leg is
identified by redirection_response, the same signal the connectors themselves already use.
A plain authorize with no payment_method is still rejected exactly as before.

PaymentsAuthorizeData::payment_method_data is not optional, so the absent instrument is
modelled explicitly as a new PaymentMethodData::NoInstrumentAfterRedirect variant rather than
substituted with a default (e.g. MandatePayment) that a connector could mistake for a payable
instrument.

Adding the variant is compiler-enforced: all 51 existing matches on PaymentMethodData now
handle it through their existing unsupported-payment-method rejection arm, so no connector's
behaviour changes and a connector that later wants to accept this leg has to opt in deliberately.
That accounts for the file count — the connector diffs are one added pattern alternative each.

PaymentsAuthorizeData::foreign_try_from derived payment_method_type from the same field and
had the same unconditional gate; it now returns None for the instrument-less leg only (matched
on NoInstrumentAfterRedirect), and errors as before otherwise.

Verification

Driven end to end against a mock Ilixium server (Hyperswitch → UCS → mock):

  • 3DS: POST /paymentsrequires_customer_action → ACS challenge → complete-authorize →
    mock received POST /platform/ili/direct/threedcomplete (merchant digest verified) → HS
    payment status succeeded. This call was rejected with InvalidArgument before the fix.
  • Direct gRPC: PaymentService/Authorize with payment_method omitted and
    redirection_response present now reaches the connector and returns CHARGED.
  • No-3DS regression: unchanged, payment succeeded.
  • Validation preserved: Authorize with no payment_method and no redirection_response
    still fails with InvalidArgument: Missing required field: payment_method.

cargo check --workspace --all-targets, cargo fmt --all and cargo clippy on the touched
crates are clean.

Context

Found while integrating the Ilixium connector (#2125). Raised separately because it is a
core-flow change that unblocks 3DS completion for every UCS connector using this pattern, not
just Ilixium.

🤖 Generated with Claude Code

Connectors that finalise 3-D Secure through the Authorize flow (Ilixium, NMI,
Airwallex, Nexixpay, ...) are re-invoked after the ACS posts back to `TermUrl`.
At that point the caller no longer holds the raw instrument, so Hyperswitch
sends `payment_method_data: None` and omits the proto field. The Authorize
handler required `payment_method` unconditionally and rejected the call with
`InvalidArgument: Missing required field: payment_method` before any connector
code ran, making 3DS completion impossible for every such connector.

`payment_method` is now required only when this is not a redirect-return leg.
The leg is identified by `redirection_response`, the same signal the connectors
themselves key off (`request.redirect_response.is_some()`). A plain authorize
with no `payment_method` is still rejected exactly as before.

`PaymentsAuthorizeData::payment_method_data` is not optional, so the absent
instrument is modelled explicitly as `PaymentMethodData::NoInstrumentAfterRedirect`
rather than substituted with a default that a connector could mistake for a
payable instrument. Every existing match on `PaymentMethodData` now handles the
new variant through its existing unsupported-payment-method rejection, so no
connector's behaviour changes; adding the variant is compiler-enforced, so a
connector that later wants to accept the leg has to opt in deliberately.

Verified end to end against a mock Ilixium server: HS /payments -> UCS ->
`POST /direct/auth`, ACS challenge, complete-authorize -> `POST
/direct/threedcomplete`, payment `succeeded`. The no-3DS authorize and the
missing-`payment_method` rejection were re-checked and are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shuklatushar226
shuklatushar226 requested review from a team as code owners August 13, 2026 18:54
@shuklatushar226

Copy link
Copy Markdown
Contributor Author

Superseded — folding this into the single Ilixium PR #2125 rather than raising a separate PR. The identical change is now on feat/grace-Illexium as commit 664e8ab, kept as its own commit so it stays reviewable in isolation.

@shuklatushar226
shuklatushar226 deleted the fix/authorize-3ds-completion-without-payment-method branch August 13, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant