Files
baya-monorepo/mvp/blocker-phases/04-payment-timezone.md
T
2026-08-02 21:27:34 +03:30

3.4 KiB

Phase 04 — The 30-minute payment countdown can lie

Blocker: blockers.md § "Payments" (the timezone item). Depends on: nothing. Small and contained. Related: 13-booking-lifecycle.md's "today's visits" fix needs a consistent timezone decision — resolve that the same way once you land this.


Root cause, precise. BookingRequest.PaymentDeadlineAt/NurseResponseDeadlineAt are DateTime (not DateTimeOffset, deliberately, per a comment about SQLite test-provider compatibility — BookingRequest.cs:53-60). The value is written correctly as UTC (AcceptBookingRequestCommand.Handler.cs:41-50), but SQL Server's datetime2 carries no timezone, and EF's SQL Server provider returns Kind = Unspecified on read — nothing resets it to Utc (BookingRequestConfig.cs:19-26 has no .HasConversion(...) for these two properties, unlike BaseEntity.CreatedAt/ModifiedAt, which correctly use DateTimeOffset). System.Text.Json therefore serializes it without a trailing Z. Date.parse() on the client (client/src/components/CountdownTimer/CountdownTimer.tsx:84) reads a Z-less ISO string as local time, not UTC — for a Tehran browser (UTC+3:30) that's ~3.5 hours added to the true deadline, so the countdown shows hours more time than actually remains and expires while still showing time left.

The fix. Add an EF Core value converter (new file, e.g. server/src/Infrastructure/Baya.Infrastructure.Persistence/ValueConversion/UtcDateTimeConverter.cs, same pattern as the existing EncryptedStringConverter.cs): convertFromProvider: v => DateTime.SpecifyKind(v, DateTimeKind.Utc) (no-op on write). Apply it to both PaymentDeadlineAt and NurseResponseDeadlineAt in BookingRequestConfig.cs:19-26. No migration needed — same column type, only the in-memory Kind tag changes on read.

Flag: the same defect class (a DateTime read back from datetime2 losing Kind) likely recurs anywhere else a server-frozen instant feeds a client countdown (e.g. dispute_window_ends_at, BNPL settled_at) — worth a grep for other DateTime (not DateTimeOffset) properties before considering this class of bug fully closed, not just the one already reported.

Follow-up from that grep (done, not yet fixed). Swept every other DateTime/DateTime? entity property (BaseEntity.CreatedAt/ModifiedAt already correctly use DateTimeOffset and are excluded). Most are internal/audit-only (payout batches, webhook events, ASP.NET Identity tables) and never reach a client. Four are real, lower-severity instances of the same bug — never wired to a live countdown, so the failure mode is "can show the wrong day near a Tehran midnight boundary," not "actively expires while showing time left":

  • Booking.DisputeWindowEndsAt (Entities/Booking/Booking.cs:95) — rendered via formatShamsiDate in BookingDetailView.tsx:444.
  • Booking.ConfirmedAt / CancelledAt / CompletedAt (Entities/Booking/Booking.cs:73,74,91) — booking timeline timestamps shown to the client.

All four are configured in BookingConfig.cs (no .HasConversion(...) today) and would take the exact same fix as this phase: .HasConversion(new UtcDateTimeConverter()). Deliberately left unfixed here — scoped out of this phase on request, tracked so it isn't dropped. Pick up as a follow-up phase (or fold into 13-booking-lifecycle.md, which already touches Booking timezone handling).