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