Skip to content

fix(connector): [WORLDPAYWPG] fix PSync/RSync response parsing - #2130

Open
pixincreate wants to merge 13 commits into
feat/worldpayxml-wallet-decrypt-recurringfrom
fix/worldpayxml-psync-response-parsing
Open

fix(connector): [WORLDPAYWPG] fix PSync/RSync response parsing#2130
pixincreate wants to merge 13 commits into
feat/worldpayxml-wallet-decrypt-recurringfrom
fix/worldpayxml-psync-response-parsing

Conversation

@pixincreate

@pixincreate pixincreate commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2122. Base is feat/worldpayxml-wallet-decrypt-recurring; retarget to main once that merges.

The bug

PaymentService/Get (PSync) and RefundService/Get (RSync) returned RESPONSE_HANDLING_FAILED for every worldpayxml order — including plain card orders with no token, so this is independent of the recurring work in #2122 and predates it.

The sync body was deserialized directly into #[serde(untagged)] WorldpayxmlTransactionResponse. Untagged forces serde's deserialize_any, whose buffered value loses XML sequence and text semantics, so the Payment variant could never match any payload:

  • balance: Option<Vec<_>> — a single <balance> buffers as a map, not a 1-element sequence.
  • <lastEvent>CAPTURED</lastEvent> buffers as {"$text": "CAPTURED"}, which a unit-variant enum cannot be built from.

WorldpayxmlWebhookResponse, whose fields were all Option, then matched unconditionally — it matches literally any input, including unrelated XML. Every real parse error was silently swallowed and resurfaced as a generic failure at transformers.rs:1699, naming neither the failing type nor the field.

Proven in a standalone repro on the same quick-xml version: even the Authorize response that parses fine today resolves to the Webhook variant when routed through that enum. Authorize/Capture/Void/Refund work only because they are typed directly.

The fix

Stop routing XML through an untagged enum. WorldpayxmlTransactionResponse no longer derives Deserialize; a hand-written impl parses a concrete, element-driven body, wraps a <paymentService> reply into Payment manually, and only then falls back to the order-notification body. Concrete type first, enum as a manually-constructed carrier, no deserialize_any anywhere.

WorldpayxmlWebhookResponse gains a required OrderCode and a typed PaymentStatus — the all-Option shape is what made it match everything.

Response hardening, folded in

  • LastEvent: added Settled, SentForAuthorisation, SentForFastRefund, RefundRequested, RefundedByMerchant, QueryRequired, CancelReceived, RefundReceived, plus #[serde(other)] Unknown. Without this PSync would have broken again the moment an order settled.
  • Status mappers take previous_status and have an explicit Unknown arm that logs a warning and retains the prior status instead of erroring or assuming a terminal state.
  • Previously-mandatory response fields wrapped in Option (AuthorisationId.@id, ResultCode.@description, Balance.@accountType/amount, PaymentMethodDetail.card, CardResponse.@type).

Expired was kept even though the reference implementation lacks it — dropping it would silently reroute a real EXPIRED event into Unknown/retain-previous, a behaviour regression.

payout_connectors/worldpayxml/transformers.rs is touched only because its status mapper matches the same shared enum and would not otherwise compile.

parse_last_event is removed entirely — with WorldpayxmlLastEvent now deserialized as a typed field, the helper is dead code. (#2122 fixes an unsafe JSON interpolation in it for the interim.)

Testing

Verified against the Worldpay sandbox. Clippy warning-free, no unwrap/expect, no suppressions, no test files.

Check Before After
Authorize → PSync RESPONSE_HANDLING_FAILED CHARGED, amount returned
CIT order with <token> → PSync RESPONSE_HANDLING_FAILED CHARGED
Refund → RSync RESPONSE_HANDLING_FAILED REFUND_PENDING (correct — Worldpay had not yet reflected it)
Parent-branch Authorize / SetupRecurring / MIT Charge pass pass — no regression

The payload that used to fail now parses, including the balance array and <lastEvent> that broke deserialize_any:

<orderStatus orderCode="..."><payment>
  <lastEvent>CAPTURED</lastEvent><AuthorisationId id="..."/>
  <paymentMethodDetail><card type="creditcard"/></paymentMethodDetail>
  <balance accountType="IN_PROCESS_CAPTURED"><amount value="1000" currencyCode="GBP" exponent="2"/></balance>
</payment></orderStatus>

An early PSync on a not-yet-available order returned Worldpay's <error code="5"> shape and correctly yielded PENDING rather than a hard failure.

🤖 Generated with Claude Code

PSync and RSync returned RESPONSE_HANDLING_FAILED for every order.

The sync response body was deserialized directly into the
`#[serde(untagged)]` WorldpayxmlTransactionResponse. Untagged forces
serde's `deserialize_any`, whose buffered value loses XML sequence and
text semantics: `balance` buffers as a map rather than a sequence and
`<lastEvent>X</lastEvent>` buffers as `{"$text": "X"}`, which a
unit-variant enum cannot be built from. The `Payment` variant therefore
never matched any payload, and the all-optional webhook variant absorbed
everything and masked the real failure.

The enum is now a carrier that is never deserialized as untagged. A
concrete, element-driven shape is parsed instead, the `<paymentService>`
envelope is wrapped into `Payment` manually, and the order-notification
body is only used as a fallback — matching how hyperswitch handles this.
`WorldpayxmlWebhookResponse` also regains mandatory `OrderCode` and a
typed `PaymentStatus` so it can no longer match arbitrary input.

Also hardens the response model against Worldpay's evolving payloads:

- Adds the Settled, SentForAuthorisation, SentForFastRefund,
  RefundRequested, RefundedByMerchant, QueryRequired, CancelReceived and
  RefundReceived events, plus `#[serde(other)] Unknown` so an
  unmodelled event no longer fails the whole response.
- Threads the previous status into the payment, mandate and refund
  status mappers so an Unknown event logs a warning and retains the
  status we already had instead of assuming a terminal one.
- Wraps previously mandatory response fields (AuthorisationId id,
  ResultCode description, Balance accountType/amount, PaymentMethodDetail
  card, CardResponse type) in Option, since Worldpay omits them
  depending on the order journey.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pixincreate pixincreate added rust Pull requests that update rust code bug Something isn't working labels Aug 14, 2026
@pixincreate pixincreate self-assigned this Aug 14, 2026
…ldpayxml-psync-response-parsing

* feat/worldpayxml-wallet-decrypt-recurring:
  fix(connector): [WORLDPAYXML] add ErrorResponse observability fields
  chore(version): 2026.08.14.0
  feat(observability): add typed connector request/response fields (#2036)
  fix(connectors): [tsys_transit] add support for psync and rsync (#2100)
  chore(version): 2026.08.13.1
  fix(grabpay,maya): added superposition urls (#2118)
@pixincreate
pixincreate marked this pull request as ready for review August 14, 2026 06:12
@pixincreate
pixincreate requested a review from a team as a code owner August 14, 2026 06:12
@pixincreate pixincreate changed the title fix(connector): [WORLDPAYXML] fix PSync/RSync response parsing fix(connector): [WORLDPAYWPG] fix PSync/RSync response parsing Aug 14, 2026
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
@hyperswitch-bot
hyperswitch-bot Bot requested review from a team as code owners August 14, 2026 06:33
…ldpayxml-psync-response-parsing

* feat/worldpayxml-wallet-decrypt-recurring:
  chore(connector): [WORLDPAYXML] regenerate probe data, docs and examples
  fix(connector): [WORLDPAYXML] escape last_event before JSON parsing
  chore: auto-fix formatting and generated code

# Conflicts:
#	crates/integrations/connector-integration/src/connectors/worldpayxml/transformers.rs
…arsing' into fix/worldpayxml-psync-response-parsing

* origin/fix/worldpayxml-psync-response-parsing:
  chore: auto-fix formatting and generated code
…ldpayxml-psync-response-parsing

* feat/worldpayxml-wallet-decrypt-recurring:
  fix(connector): read calida shop name from connector specific config (#2106)
…ldpayxml-psync-response-parsing

* feat/worldpayxml-wallet-decrypt-recurring:
  test(connector): [WORLDPAYXML] declare the recurring suites in specs.json
…recurring' into fix/worldpayxml-psync-response-parsing

* origin/feat/worldpayxml-wallet-decrypt-recurring:
  fix(connector): [WORLDPAYXML] fail token-less mandate setup, surface MIT declines
…recurring' into fix/worldpayxml-psync-response-parsing

* origin/feat/worldpayxml-wallet-decrypt-recurring:
  revert(connector): [WORLDPAYXML] keep token-less mandate setup lenient
…recurring' into fix/worldpayxml-psync-response-parsing

* origin/feat/worldpayxml-wallet-decrypt-recurring:
  refactor(connector): [WORLDPAYXML] address review feedback on 2122

# Conflicts:
#	crates/integrations/connector-integration/src/connectors/worldpayxml/transformers.rs
…recurring' into fix/worldpayxml-psync-response-parsing

* origin/feat/worldpayxml-wallet-decrypt-recurring:
  refactor(connector): [WORLDPAYXML] convert the remaining two helpers to From impls
…recurring' into fix/worldpayxml-psync-response-parsing

* origin/feat/worldpayxml-wallet-decrypt-recurring:
  refactor(connector): [WORLDPAYXML] convert the refund status mapping to a From impl

# Conflicts:
#	crates/integrations/connector-integration/src/connectors/worldpayxml/transformers.rs
…recurring' into fix/worldpayxml-psync-response-parsing

* origin/feat/worldpayxml-wallet-decrypt-recurring:
  docs(grace): require superposition URL registration + URL patching for new connectors (#2123)
  chore(version): 2026.08.14.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants