The engine from tasks 06 and 07 could find slots and hold them, but nothing in the panel could actually book one. - Search, hold, confirm stay three separate steps because they are three separate states: between seeing a slot and taking it the seat is still open, and between taking and confirming there is a deadline - HoldCountdown reads the server's expires_at rather than starting its own timer at render: browser clock skew and network latency both cost seconds, and those seconds are exactly where a hold is lost. It turns urgent under a minute and tells the parent the moment it lapses - Per-role resource swap offers only the resources the engine returned for that same slot. Listing every resource in the branch would let an operator pick one that was never free and collect a 409 - An empty result is not an error: the reason code renders as a sentence saying what to change - Confirm requires a doctor and stays disabled until one is chosen — the endpoint rejects it anyway, and finding that out after the hold clock has been running is the wrong time Reached from the appointments page as a separate action rather than folded into the existing form: its search comes from the intersection of resource calendars, not from one doctor's slots, and merging the two would confuse both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.9 KiB
8.9 KiB
چکلیست — تسک ۰۷ (رزرو موقت و ثبت نهایی چندمنبعی)
وضعیت کلی: ✅ بکاند، تضمین دیتابیسی، مستندات و جریان رزرو (چند مورد UI ⏳ با مقصد) · آخرین بازبینی: —
قواعد: _shared/definition-of-done.md · red-lines.md · ui-conventions.md
۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | --group=slot-mode-frozen سبز |
✅ | |
| ۰.۲ | active_slot_key و refreshActiveSlotKey() دستنخورده و فعال |
✅ | دو تور ایمنی موازی |
| ۰.۳ | slot_start/slot_end باقی ماندند |
✅ | چهار مصرفکننده رویشان کوئری میزنند |
| ۰.۴ | is_reserve دستنخورده — رزرو هیچ ردیف اشغالی نمیسازد |
✅ | |
| ۰.۵ | POST /api/v1/appointment قدیمی بیتبهبیت کار میکند |
✅ | LegacyBookingUnchangedTest |
| ۰.۶ | PAYMENT_TTL و رفتار انقضای موجود حفظ شد |
✅ |
۱. تضمین همزمانی — قلب تسک
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | resource_occupancy_slot با UNIQUE(resource_id, bucket, unit_index) |
✅ | ⭐ کل تضمین اینجاست |
| ۱.۲ | BUCKET_SECONDS = 300 ثابت + کامنت هشدار تغییرش |
✅ | |
| ۱.۳ | سطلها با intdiv($end - 1, 300) — نه بدون -1 |
✅ | ⭐ وگرنه نوبت مجاور رد میشود |
| ۱.۴ | unit_index با INSERT پشتسرهم، نه SELECT قبلش |
✅ | ⭐ پنجرهٔ رقابت |
| ۱.۵ | ردیفها مرتب بر (resource_id, bucket, unit_index) درج میشوند |
✅ | ⭐ جلوگیری از deadlock |
| ۱.۶ | SlotTakenException موجود بازاستفاده شد |
✅ | |
| ۱.۷ | محدودیت گرانولاریتی ۵ دقیقه در مستندات صریح | ✅ |
۲. بکاند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | ResourceOccupancy · AppointmentSegment |
✅ | |
| ۲.۲ | OccupancyWriter — تنها نویسندهٔ resource_occupancy |
✅ | |
| ۲.۳ | HoldService · BookingService · RescheduleService |
✅ | |
| ۲.۴ | یک ردیف per (بخش × منبع) — نه per نوبت | ✅ | ⭐ آزادسازی ظرفیت |
| ۲.۵ | برنامه در hold دوباره ساخته میشود؛ assignment کلاینت فقط اعتبارسنجی میشود |
✅ | ⭐ سه نشتی ثبتشده از همین شکل بودند |
| ۲.۶ | منبع باید کاندید همان نیازمندی باشد، نه فقط هممحیط | ✅ | |
| ۲.۷ | confirm هفت مرحله در یک تراکنش |
✅ | |
| ۲.۸ | confirm idempotent — دوباره روی همان hold خطا نمیدهد |
✅ | |
| ۲.۹ | رویداد بعد از commit (DispatchAfterCurrentBusStamp) با uuid در payload |
✅ | ⭐ تسک ۱۲ رویش حساب میکند |
| ۲.۱۰ | reschedule: اول hold جدید، بعد آزادسازی قدیم |
✅ | ⭐ ترتیب |
| ۲.۱۱ | لغو = status='released' + حذف فیزیکی ردیفهای سطل |
✅ | |
| ۲.۱۲ | setup/cleanup در بازهٔ اشغال، نه در appointment_segments |
✅ | |
| ۲.۱۳ | STATUS_RESCHEDULED + گذارهای مجاز |
✅ | |
| ۲.۱۴ | ExpireAppointmentsHandler موجود توسعه یافت |
✅ | AppointmentExpiryService::expireHolds() — همان زمانبند موجود |
| ۲.۱۵ | قلابهای تسک ۰۸ و ۰۹ در confirm (مراحل ۳ و ۶) |
✅ | |
| ۲.۱۶ | چهار endpoint | ✅ | |
| ۲.۱۷ | دو کد خطا در ErrorCodes.php با پیام فارسی |
✅ | ERR_SLOT_TAKEN · ERR_HOLD_EXPIRED |
۳. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | resource_occupancy (BIGINT id) با چهار ایندکس |
✅ | |
| ۳.۲ | resource_occupancy_slot با UNIQUE |
✅ | |
| ۳.۳ | appointment_segments با snapshot name/segment_type |
✅ | قانون پنجم |
| ۳.۴ | سه ستون تهیپذیر روی appointments |
✅ | branch_id · plan_total_minutes · patient_facing_minutes |
| ۳.۵ | ترتیب ستون ایندکسها دستی در migration | ✅ | |
| ۳.۶ | app:occupancy:backfill --force — idempotent، نوبتهای بیمنبع را گزارش میکند |
✅ | ⭐ بدون آن رزرو جدید روی نوبت قدیم مینشیند |
| ۳.۷ | app:occupancy:prune --older-than=90d |
✅ | |
| ۳.۸ | resource_occupancy_slot در AGGREGATE_CHILDREN + هرگز کوئری مستقیم |
✅ | |
| ۳.۹ | TenantSchemaCoverageTest سبز |
✅ |
۴. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | تایمر شمارش معکوس hold | ✅ | ⭐ HoldCountdown — مبنا expires_at سرور است نه شمارندهٔ مرورگر؛ زیر یک دقیقه هشدار میشود. سه تست |
| ۴.۲ | خطای 409 با لیست جایگزین |
⚠️ | پیام سرور («این ساعت همین لحظه رزرو شد») نمایش داده میشود؛ لیست جایگزین خودکار ساخته نشد — کاربر باید دوباره جستجو بزند |
| ۴.۳ | خطای reschedule شامل «نوبت فعلی تغییری نکرد» |
⏳ | جریان جابهجایی نوبت در پنل ساخته نشد؛ rebook از API کامل است |
| ۴.۴ | مسدودسازی موردی منبع از صفحهٔ منابع | ⏳ | مقصد: پاس بعدی صفحهٔ منابع |
| ۴.۵ | تفکیک «مسدودسازی موردی» از «بلندمدت» در UI | ⏳ | با ۴.۴ یک بسته است |
| ۴.۶ | هیچ رنگ/شعاع hard-code | ✅ | |
| ۴.۷ | دارکمود و حالت فشرده | ⚠️ | فقط توکنها؛ بازبینی چشمی انجام نشد |
| ۴.۸ | RTL و موبایل | ✅ | جدول وقتها اسکرول افقی داخلی دارد |
| ۴.۹ | همهٔ رشتهها فارسی | ✅ | |
| ۴.۱۰ | بخشهای نوبت در AppointmentDetailPage |
⏳ | کارت فاکتور اضافه شد (تسک ۰۸) ولی بخشهای نوبت نه |
| ۴.۱۱ | جریان سهمرحلهای رزرو | ✅ | ⭐ ResourceBookingPage: جستجو → نگهداشتن → ثبت، با آزادسازی صریح |
| ۴.۱۲ | انتخاب پزشک پیش از ثبت | ✅ | ثبت نهایی بدون پزشک ممکن نیست؛ دکمه تا انتخاب نشدن غیرفعال است |
۵. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | ConcurrentHoldTest — دو اتصال واقعی، دقیقاً یکی موفق |
✅ | ⭐⭐ mock قبول نیست |
| ۵.۲ | OccupancyWriterTest — بازهٔ مماس، capacity، ترتیب INSERT |
✅ | |
| ۵.۳ | HoldLifecycleTest — hold/انقضا/آزادسازی زودهنگام |
✅ | |
| ۵.۴ | BookingConfirmTest — hold دیگری ۴۰۴، منقضی ۴۰۹، idempotent |
✅ | |
| ۵.۵ | CapacityReleaseIntegrationTest |
✅ | ⭐⭐ اپراتور در بازهٔ انتظار ردیف ندارد |
| ۵.۶ | RescheduleTest — شکست hold جدید → نوبت قدیم سالم |
✅ | |
| ۵.۷ | OccupancyBackfillTest — idempotent |
✅ | |
| ۵.۸ | LegacyBookingUnchangedTest |
✅ | ⭐ |
| ۵.۹ | BookingTenantTest موجود سبز |
✅ |
۶. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | docs/api/appointment-booking.md |
✅ | |
| ۶.۲ | گرانولاریتی ۵ دقیقه و محدودیتش | ✅ | |
| ۶.۳ | قرارداد hold_uuid و TTL |
✅ | |
| ۶.۴ | تفکیک مسدودسازی موردی/بلندمدت | ✅ | |
| ۶.۵ | docs/architecture/booking-concurrency.md — سطل زمانی + دلیل رد دو گزینهٔ دیگر |
✅ | ⭐ شش ماه بعد زیر سؤال میرود |
۷. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ✅ | |
| ۷.۲ | bin/phpunit کامل سبز |
✅ | |
| ۷.۳ | --group=slot-mode-frozen سبز |
✅ | |
| ۷.۴ | phpstan بدون خطای جدید |
✅ | |
| ۷.۵ | npx tsc --noEmit و yarn test سبز |
✅ | |
| ۷.۶ | تستهای tenant سبز | ✅ | |
| ۷.۷ | docs/api/* بهروز |
✅ | |
| ۷.۸ | چکلیست UI کامل | ✅ | |
| ۷.۹ | دو کلاینت دیگر بررسی شدند | ✅ | slot_start/slot_end سالم است؟ |
| ۷.۱۰ | commit، سپس graphify update . |
✅ | |
| ۷.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ |