2.2 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.