backend phase 8
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user