fix(connector): [WORLDPAY] parse refused responses when refusalCode or refusalDescription is missing - #14263
fix(connector): [WORLDPAY] parse refused responses when refusalCode or refusalDescription is missing#14263errmakov wants to merge 2 commits into
Conversation
…r refusalDescription is missing `WorldpayPaymentResponseFields` was an untagged enum whose `RefusedResponse` variant required both `refusalCode` and `refusalDescription`. A refusal carrying only one of them matched no variant, so the flattened `other_fields` silently became `None` and the refusal code and message were dropped. Deserialize `WorldpayPaymentsResponse` by reading `outcome` first and parsing the remaining fields into the shape that outcome implies, as suggested in the review of juspay#8763. `refusalCode` and `refusalDescription` are now optional. When one is missing, the error response falls back to `NO_ERROR_CODE` / `NO_ERROR_MESSAGE`, and the network decline code and network error message are left unset rather than filled with placeholders. Fixes juspay#8749 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Changed Files
|
|
[should-fix] A type mismatch or unexpected shape in a Worldpay response will be discarded as |
|
Replace the custom |
|
[should-fix] Deserialization failure is silently dropped
|
…r outcome `parse_fields` turned a deserialization error into `None` without a trace, so a malformed Worldpay response was indistinguishable from one that carries no extra fields. Log a warning with the outcome, the expected fields type and the error before returning `None`. Parsing stays lenient on purpose: capture, void and refund responses carry only `_links`, and a refusal with one unexpected value should still be recorded as a refusal rather than failing the whole response. The logged error omits response values, which serde quotes in messages such as `invalid type: string "..."`; only `missing field` messages are kept verbatim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@XyneSpaces thanks for the review. Addressed in fb6cb31. Silently dropped errors. I kept the result lenient rather than propagating the error:
|
|
[should-fix] The Either exhaustively map all refusal variants or preserve the raw refusal object in the fallback arm instead of returning |
|
@XyneSpaces I think this one doesn't match the code in this PR: there is no The closest
A |
Type of Change
Description
WorldpayPaymentResponseFieldswas an untagged enum, and itsRefusedResponsevariant required bothrefusalCodeandrefusalDescription. When Worldpay refuses a payment and sends only one of them, the body matches no variant, and becauseother_fieldsis a flattenedOption, serde silently deserializes it asNoneinstead of failing. The payment is still markedFailure(fromoutcome: refused), but the connector response carries no error code or message, so the decline reason is lost.Checked against the pre-fix types:
{"outcome": "refused", "refusalDescription": "Do not honour"}deserializes toOk(WorldpayPaymentsResponse { outcome: Refused, other_fields: None }).As suggested in the review of #8763, simply making the two fields optional isn't enough: with an untagged enum, almost any body would then match
RefusedResponse. This PR:Implements
DeserializeforWorldpayPaymentsResponseby hand (crates/hyperswitch_connectors/src/connectors/worldpay/response.rs). It readsoutcomeandtransactionReference, then parses the remaining fields into the shape that outcome implies:refused→RefusedResponse3dsDeviceDataRequired→DDCResponse3dsChallenged→ThreeDsChallengedfraudHighRisk→FraudHighRiskauthorized,sentForSettlement,sentForRefund,sentForPartialRefund,sentForCancellation,3dsAuthenticationFailed,3dsUnavailable→AuthorizedResponseEvery outcome is matched explicitly. A body that doesn't match its outcome's shape (e.g. a cancellation response with only
_links) still has no extra fields, as before, but the mismatch is now logged as awarnwith the outcome, the expected fields type and the error. The logged error omits response values; onlymissing fieldmessages are kept verbatim.Removes the derived
DeserializefromWorldpayPaymentResponseFields(it keepsSerialize), so the ambiguous untagged deserialization can't be used by accident.Makes
RefusedResponse.refusal_codeandrefusal_descriptionOption<String>, keeping the existing comment on the raw response code.In
crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs, when a refusal field is missing,ErrorResponse.code/messagefall back toNO_ERROR_CODE/NO_ERROR_MESSAGE.reason,network_decline_codeandnetwork_error_messageare leftNonerather than filled with a placeholder, so a placeholder is never reported as coming from the card network.Additional Changes
Motivation and Context
Fixes #8749
Supersedes #8763, which stalled on the review asking for a custom deserializer.
How did you test it?
Unit tests (
connectors::worldpay::response::tests, 13 new), run withcargo test -p hyperswitch_connectors --features v1,payouts,frm --lib worldpay::response::tests:refusedwithrefusalCode,refusalDescription,advice,riskFactorsRefusedResponse, all fields keptrefusedwith onlyrefusalDescriptionRefusedResponse, codeNonerefusedwith onlyrefusalCodeRefusedResponse, descriptionNonerefusedwith neitherRefusedResponse, bothNoneauthorizedwithpaymentInstrument,_links,_actionsAuthorizedResponse3dsDeviceDataRequiredDDCResponse3dsChallengedThreeDsChallengedfraudHighRiskFraudHighRisksentForCancellationwith only_linksrefusedwith a numericrefusalCodeoutcomeTwo more tests cover the logged error detail: a missing field keeps its name, and a mistyped value is not echoed.
The tests failed to compile before the change (refusal fields were
String) and all 13 pass after it.cargo +nightly fmtapplied.cargo clippy -p hyperswitch_connectors --features v1,payouts,frm --all-targetsreports no warnings inhyperswitch_connectors.Checklist
cargo +nightly fmt --allcargo clippy🤖 Generated with Claude Code