backend phase 8

This commit is contained in:
hamid
2026-07-06 02:48:56 +03:30
parent 99ebf5d881
commit 2cfc082a04
55 changed files with 7480 additions and 7 deletions
+24
View File
@@ -332,6 +332,30 @@ rules this establishes:
and the wire carry `in_review`, not `InReview`. Enum→code mapping in a projected read happens **in memory
after materialization** (`.ToCode()` is not LINQ-translatable); DTOs expose the code string.
### Forward-only status machine (backend-phase-8)
When an entity has a lifecycle `status` with a fixed set of allowed transitions, model the machine as a
**static allowed-edges table** and route **every** write through it — never assign `status` ad-hoc. The b8
pattern (reused by b9 for the `bookings` machine):
- **Statuses are `const string` codes** (`BookingRequestStatus`) persisted as the stable snake_case string —
no C# enum, no value converter needed. **Edges live in a static `CanTransition(from, to)`**
(`BookingRequestTransitions`) built from a `Dictionary<string, IReadOnlyCollection<string>>`; terminal
states map to an empty set.
- **The entity owns the transition.** `status` has a **private setter**; the only mutators are cohesive domain
methods (`Accept`/`Reject`/`Cancel…`) that call a private `Transition(target)` which asserts the edge is
legal (throws on an illegal edge — a programming error, since the handler pre-checks). Side-effect fields
(`payment_deadline_at`, `rejection_reason`) are set in the same method.
- **The handler pre-checks and returns a clean 409.** `if (!entity.CanTransitionTo(target)) return
OperationResult.ConflictResult(...)` — never throw for the expected "already moved / terminal" case.
- **Time-sensitive commands self-guard** against a passed deadline via `IDateTimeProvider` rather than trusting
a sweep has run; the recurring expiry `BackgroundService` is bounded/paginated/idempotent, and its
`WHERE status = …` predicate (re-queried each tick) is the concurrency guard — a row a racing action moved is
simply not reloaded.
- **Deadline columns that are compared/sorted use `DateTime` (UTC `datetime2`), not `DateTimeOffset`** — the
SQLite test provider cannot translate `DateTimeOffset` comparison/`ORDER BY`. Order lists/sweeps by `Id`, not
the timestamp, for the same reason.
---
## 7. Validation