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

48 lines
3.4 KiB
Markdown

# 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](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](13-booking-lifecycle.md), which already touches `Booking` timezone handling).