release: dev → prod — 2026-07-17 #2 (refund gateway wiring + booking lifecycle correctness + request-list perf) - #1014
Conversation
) refundPayment marked refunds SUCCEEDED and ran the full money cascade without ever calling the gateway — the customer's card was never credited, and a later manual dashboard refund double-posted the cascade via a second webhook-minted Refund row (audit M1). Three-phase flow now: (1) Serializable reserve — balance re-check + PENDING row with a pending_ placeholder the reconcile cron recognizes; (2) gateway call outside any transaction, mirroring freeze-appointments; (3) bind the real gateway refund id in its own write, then cascade + SUCCEEDED atomically only when the gateway confirms. Gateway-PENDING refunds defer the cascade to the refund webhook (which now finds OUR row by gateway id — no duplicate row); gateway declines mark the row FAILED (payer notified via #779) without consuming refundable balance; gateway throws keep the placeholder for reconcile-or-24h-FAIL. A cascade failure after settlement no longer rolls back the Refund row — money moved, so the row survives PENDING under its gateway id and the webhook/backstop cron re-drive it. Also corrects the freeze-appointments comment claiming the reconcile cron "retries" gateway refunds — it only matches existing ones (M8); re-initiation stays out until gateway-side idempotency keys exist. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
✅ Deploy Preview for familiarise ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
There was a problem hiding this comment.
Code Review
This pull request implements a robust three-phase refund process that integrates real gateway refund calls into the refund operation workflow, updating corresponding tests to cover pending, failed, and successful gateway scenarios. The review feedback highlights critical issues in lib/payments/operations/refund.ts where directly overwriting the metadata field with gateway metadata would cause data loss by wiping out existing fields. Additionally, the feedback points out that gateway.refundId could be falsy, which would violate the RefundResult type contract or cause database update failures, and suggests merging metadata and using reserved.refundId as a fallback.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| await prisma.refund.update({ | ||
| where: { id: reserved.id }, | ||
| data: { | ||
| refundId: gateway.refundId, | ||
| ...(gateway.metadata | ||
| ? { metadata: gateway.metadata as Prisma.InputJsonValue } | ||
| : {}), | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Overwriting the metadata field directly with gateway.metadata will completely replace the existing JSON object in the database. This causes critical data loss, wiping out the initiatedByUserId and source: "app" fields that were set during Phase 1.
Additionally, if gateway.refundId is falsy (e.g., if the gateway fails to return an ID), updating the non-nullable unique refundId field will fail. We should merge the existing metadata with the gateway metadata and fall back to reserved.refundId if gateway.refundId is falsy.
const mergedMetadata = gateway.metadata
? {
...(reserved.metadata && typeof reserved.metadata === "object" ? reserved.metadata : {}),
...(gateway.metadata as Record<string, unknown>),
}
: undefined;
await prisma.refund.update({
where: { id: reserved.id },
data: {
refundId: gateway.refundId || reserved.refundId,
...(mergedMetadata ? { metadata: mergedMetadata as Prisma.InputJsonValue } : {}),
},
});| organizationEarningsReversed: 0, | ||
| clawbackInitiated: false, | ||
| status: "PENDING" as const, | ||
| gatewayRefundId: gateway.refundId, |
There was a problem hiding this comment.
If gateway.refundId is falsy, returning it directly as gatewayRefundId will violate the RefundResult type contract (which expects a non-nullable string) and cause runtime issues. Fall back to reserved.refundId to ensure a valid string is always returned.
| gatewayRefundId: gateway.refundId, | |
| gatewayRefundId: gateway.refundId || reserved.refundId, |
| amountRefundedPaise: requested, | ||
| ...cascade, | ||
| status: "SUCCEEDED" as const, | ||
| gatewayRefundId: gateway.refundId, |
There was a problem hiding this comment.
…1015) Release #1014 review, on code from #1001: - Phase-3a gateway-id binding replaced the Refund row's metadata JSON wholesale, destroying Phase-1 audit keys (initiatedByUserId, source) on every Razorpay refund (notes always present). Now spreads the reserved row's metadata under the gateway keys. - refundId binding gains the same falsy-guard as the FAILED branch, so the unique non-nullable column never gets "" and the pending_ placeholder stays matchable by reconcile-pending-refunds. - RefundResult.gatewayRefundId is now optional and normalized with `|| undefined` at both return sites: absent, never "". No caller consumed the field; the reconcile cron keys off the DB column. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>




PR #1001 wires app-initiated refunds to the actual Razorpay gateway, closing the gap where refund requests were recorded but never actually issued through the payment processor.
PR #1002 (lifecycle correctness — cancelled-event CAS, class partial reschedule, cleanup grace, utilization double-debit, reminder scheduler workflow) did NOT make this release: its CI run failed a real unit test (
__tests__/booking/cleanup-tentative-guard.test.ts, "cleanup delete re-states the tentative + unpaid guards") — the cleanup sweep'sdeleteManycall no longer carries theisTentative+ no-SUCCEEDED-payment WHERE guards. It needs a fix and a green re-run before it can go out.PR #1000 (narrows the PENDING request queries and moves Auto Allocate server-side) also did NOT make this release: its SonarCloud quality gate failed on new code duplication (25.0% vs. the 3% threshold on
new_duplicated_lines_density). It needs deduplication before it can go out.No DB schema changes or migrations are included in this release.
🤖 Generated with Claude Code