48 lines
3.4 KiB
Markdown
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).
|