Skip to content

Carry orderId (credit order) on the refund webhook events - #69

Merged
sandervanhooft merged 5 commits into
mainfrom
fix/refund-event-order-id
Nov 2, 2026
Merged

sandervanhooft merged 5 commits into
mainfrom
fix/refund-event-order-id

Conversation

@sandervanhooft

@sandervanhooft sandervanhooft commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Carries orderId, the credit order Vatly creates from a refund (whose customerInvoice is the credit note), on the three refund webhook DTOs: RefundCompleted, RefundFailed, and RefundCanceled.

Why

The refund webhook payload is fat and signed (byte-identical to GET /v1/refunds/{id}), and WebhookEventFactory already hydrates the full Refund resource from it with zero network round-trip. But fromApiRefund() copied only a subset and dropped orderId. A consumer that needs the credit note (the customerInvoice of the credit order) therefore had to call GetRefund($refundId) again purely to recover the credit-order id, re-fetching data that was in hand a moment earlier, before the single legitimate GetOrder.

This surfaced in the FluentCart Merchant-of-Record gateway's credit-note reaction, which does GetRefund then GetOrder on every refund.completed. Exposing orderId lets that collapse to a single GetOrder.

Changes

  • RefundCompleted, RefundFailed, RefundCanceled: new public ?string $orderId = null constructor-promoted property, mapped from $refund->orderId in fromApiRefund(). Added last with a null default, so the change is backward-compatible for both positional and named construction.
  • Corrected the stale docblock on all three (which claimed the factory "enriches via GetRefund before dispatching") to describe what the factory actually does: hydrate from the fat, signed payload with no round-trip.
  • CHANGELOG.md: entry under Unreleased.
  • tests/Webhooks/Events/RefundEventsTest.php: assert orderId maps through, and defaults to null when the refund has no credit order yet.

Testing

  • composer test: 284 tests green.
  • composer phpstan (with --memory-limit=1G): no errors.

Notes

orderId is typically populated for refund.completed (a credit order exists) and null for refund.failed and refund.canceled. It is carried on all three for a uniform DTO shape.

RefundCompleted/RefundFailed/RefundCanceled dropped the refund's orderId -
the credit order Vatly creates from a refund, whose customerInvoice is the
credit note - even though it is already present in the signed refund payload
the factory hydrates from. A consumer stamping the credit note therefore had
to re-fetch the refund with GetRefund just to recover that id, a redundant
round-trip. Expose orderId (?string, null until a credit order materializes)
so the credit note is one GetOrder away.

Also corrects the stale 'enriches via GetRefund before dispatching' docblock:
the factory hydrates these events from the fat, signed webhook payload with
no network round-trip.
The list samples for checkouts, customers, subscriptions, refunds and
chargebacks called a list() method that does not exist. The refund samples
called refunds->create(), and the checkout samples called getCheckoutUrl().

Replace them with working calls, and align the affected reference tables with
the resources and the published API spec.
@sandervanhooft
sandervanhooft merged commit 502dc81 into main Nov 2, 2026
12 checks passed
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