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 viaformatShamsiDateinBookingDetailView.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).