-
-
Notifications
You must be signed in to change notification settings - Fork 599
Description
Bug Description
Refunds initiated via the "Refund Order" UI remain stuck at REFUND_PENDING indefinitely,
even though Stripe successfully processes the refund. The order never transitions to
REFUNDED.
Root Cause
IncomingWebhookHandler registers Event::REFUND_UPDATED (refund.updated) in its
$validEvents array and switch statement. However, for standard card refunds created via
$stripeClient->refunds->create(), Stripe fires charge.refunded — not refund.updated.
refund.updated is only fired for asynchronous refund status transitions (e.g., bank
transfer refunds going from pending → succeeded). For card refunds, which complete
synchronously, Stripe fires charge.refunded and never sends refund.updated.
As a result, the webhook event is never delivered to the endpoint (since it's not
subscribed), and even if charge.refunded were added to the Stripe webhook subscription,
the app would silently discard it at the $validEvents check.
Steps to Reproduce
- Complete a card payment for an order
- From the Orders page, click "Refund Order" and submit a refund
- Verify in Stripe Dashboard that the refund succeeds
- Observe the order remains at REFUND_PENDING in Hi.Events
- Check Stripe Dashboard → Developers → Events: charge.refunded was fired, but no
refund.updated exists - Check Stripe Dashboard → Webhooks → Event Deliveries: no refund.updated delivery
Expected Behavior
After Stripe processes the refund, the order should transition to REFUNDED (or
PARTIALLY_REFUNDED) and an order_refunds record should be created.
Suggested Fix
In
backend/app/Services/Application/Handlers/Order/Payment/Stripe/IncomingWebhookHandler.php:
- Add Event::CHARGE_REFUNDED to $validEvents
- Add a switch case that extracts the Refund object(s) from the Charge payload and passes
them to the existing ChargeRefundUpdatedHandler
// In $validEvents array:
Event::CHARGE_REFUNDED,
// In switch statement:
case Event::CHARGE_REFUNDED:
$this->handleChargeRefunded($event->data->object);
break;
// New private method:
private function handleChargeRefunded(Charge $charge): void
{
$refunds = $charge->refunds->data ?? [];
foreach ($refunds as $refund) {
$this->refundEventHandlerService->handleEvent($refund);
}
}
The existing idempotency check in ChargeRefundUpdatedHandler (lookup by refund_id)
prevents duplicate processing.
Users also need to add charge.refunded to their Stripe webhook endpoint's subscribed
events.
Environment
- Stripe API version: 2025-07-30.basil (but this is not version-specific — charge.refunded
has been the standard event for card refunds across all recent API versions) - Hi.Events: latest develop branch