Skip to content

release: dev → prod — 2026-07-17 #2 (refund gateway wiring + booking lifecycle correctness + request-list perf) - #1014

Merged
teetangh merged 1 commit into
prodfrom
release/dev-to-prod-2026-07-17-2
Jul 17, 2026
Merged

release: dev → prod — 2026-07-17 #2 (refund gateway wiring + booking lifecycle correctness + request-list perf)#1014
teetangh merged 1 commit into
prodfrom
release/dev-to-prod-2026-07-17-2

Conversation

@teetangh

Copy link
Copy Markdown
Contributor

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's deleteMany call no longer carries the isTentative + 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

)

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>
@netlify

netlify Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploy Preview for familiarise ready!

Name Link
🔨 Latest commit 7a447c6
🔍 Latest deploy log https://app.netlify.com/projects/familiarise/deploys/6a5a2ce002e4220009205371
😎 Deploy Preview https://deploy-preview-1014--familiarise.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 83 (🟢 up 20 from production)
Accessibility: 99 (🟢 up 3 from production)
Best Practices: 92 (🟢 up 9 from production)
SEO: 100 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f93c8217-51fd-4ba9-8f44-ac8e68c1f33d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/dev-to-prod-2026-07-17-2

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +335 to +343
await prisma.refund.update({
where: { id: reserved.id },
data: {
refundId: gateway.refundId,
...(gateway.metadata
? { metadata: gateway.metadata as Prisma.InputJsonValue }
: {}),
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
gatewayRefundId: gateway.refundId,
gatewayRefundId: gateway.refundId || reserved.refundId,

amountRefundedPaise: requested,
...cascade,
status: "SUCCEEDED" as const,
gatewayRefundId: gateway.refundId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If gateway.refundId is falsy, returning it directly as gatewayRefundId will violate the RefundResult type contract. Fall back to reserved.refundId to ensure a valid string is always returned.

Suggested change
gatewayRefundId: gateway.refundId,
gatewayRefundId: gateway.refundId || reserved.refundId,

@teetangh
teetangh merged commit 654c4cd into prod Jul 17, 2026
21 checks passed
@teetangh
teetangh deleted the release/dev-to-prod-2026-07-17-2 branch July 17, 2026 13:33
teetangh added a commit that referenced this pull request Jul 17, 2026
…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>
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