Record which resource an appointment was booked for, and freeze its numbers

An appointment could say which services it was for but not which resource
performed them, so a booking on laser #2 was indistinguishable from one on
laser #1. Both columns are nullable: the appointments that already exist have
no resource and the migration must not break them.

resource_id is not a duplicate of resource_occupancy. Occupancy records what
was held and when — including rooms and devices held for a single segment. This
column records what the appointment is *for*, which is what the panel lists and
what the patient chose.

The option is kept separately from service_item because duration and price
resolve from the resource+service+option triple; without knowing the option,
the stored number cannot be explained later.

Tests: the resource and option survive a round-trip, stored minutes come from
the resolver rather than the service default (15 where the service says 30),
raising the tariff afterwards leaves the earlier snapshot at 8M, and an
appointment with no resource still serialises with nulls instead of failing.

Suite 1290 green, phpstan at its 14-error baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-01 21:48:45 +03:30
co-authored by Claude Opus 5
parent 6d7c54508c
commit 0a2ba88808
4 changed files with 295 additions and 7 deletions
@@ -61,13 +61,13 @@
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `Appointment::$resource` تهی‌پذیر | | |
| ۴.۲ | `Appointment::$serviceOptionItem` تهی‌پذیر | | |
| ۴.۳ | `service_total_minutes` از resolver پر می‌شود | | |
| ۴.۴ | `PriceSnapshot` از resolver پر می‌شود | | |
| ۴.۵ | `toArray()` منبع و گزینه را برمی‌گرداند | | |
| ۴.۶ | migration بدون شکستن نوبت‌های موجود | | |
| ۴.۷ | تست: تغییر قیمت سرویس، snapshot قدیمی ثابت | | |
| ۴.۱ | `Appointment::$resource` تهی‌پذیر | | `Appointment::$resource` تهی‌پذیر با `ON DELETE SET NULL` |
| ۴.۲ | `Appointment::$serviceOptionItem` تهی‌پذیر | | `Appointment::$serviceOptionItem` تهی‌پذیر |
| ۴.۳ | `service_total_minutes` از resolver پر می‌شود | | `testTheStoredMinutesComeFromTheResolverNotTheServiceItself` — ۱۵ به‌جای ۳۰ |
| ۴.۴ | `PriceSnapshot` از resolver پر می‌شود | | `PriceSnapshot` از خروجی resolver پر می‌شود |
| ۴.۵ | `toArray()` منبع و گزینه را برمی‌گرداند | | `toArray()` کلیدهای `resource` و `service_option` را می‌دهد |
| ۴.۶ | migration بدون شکستن نوبت‌های موجود | | `Version20260801181433` — دو ستون تهی‌پذیر؛ نوبت‌های موجود دست‌نخورده |
| ۴.۷ | تست: تغییر قیمت سرویس، snapshot قدیمی ثابت | | `testChangingTheServicePriceLaterLeavesTheSnapshotAlone` — تعرفه ۸M→۱۲M، snapshot ۸M ماند |
## ۵. رزرو با منبع در endpointها