Files
clinicpro/docs/new_feture/taskes/task-07-hold-and-book/checklist.md
T
hamedandClaude Opus 5 4395eea56e feat(booking): multi-resource holds and confirmation with a database-level guarantee
Section 11 and the third closing rule of the design document: preventing a double
booking is the database's job, not the code's. Any "is it free?" check in PHP has a
race window between the read and the write — two concurrent requests both see free
and both write.

MariaDB has no range EXCLUDE constraint, so every occupied interval is broken into
fixed five-minute buckets under UNIQUE(resource_id, bucket_at, seat). The code only
INSERTs; a rejection from the database *is* the answer. `seat` carries capacity: a
three-bed room has seats 0..2, allocation walks upward on each collision, and the
fourth concurrent hold finds nowhere to sit. Counting capacity in PHP would have
rebuilt the very race this removes.

Buckets are written through DBAL rather than the ORM on purpose: a unique violation
raised inside flush() closes the EntityManager, and the next seat attempt would then
fail with "EntityManager is closed", hiding the real outcome.

Occupancy is one row per (segment × resource). The reference test asserts the payoff
directly: for a 55-minute appointment of numbing / waiting / laser, the room gets
three rows and the operator only two — the operator holds nothing during the wait and
stays bookable for someone else.

A partial hold never survives. If the second resource has no room, the first is
released and the hold itself removed; otherwise a resource stays locked for an
appointment that will never exist.

Confirming does not re-reserve anything — the seats were taken at hold time and only
the label changes. Re-reserving on confirm would reopen the race the hold closed.
Cancelling marks rows `released` instead of deleting them, because the history of
which resource was busy when is the input to the utilisation reports; the uniqueness
buckets *are* deleted, or that interval would stay locked forever.

Expired holds are released by the existing scheduler rather than a new one. That
exposed a bug in my own change: the flush guard used $count, which now includes
released holds, so reset([]) could pass false to save(). It is guarded on $expired.

The appointment itself is still built with the existing constructor, so
active_slot_key, events and the payment path behave exactly as before — the
multi-resource occupancy sits beside them, not instead of them.

12 tests. Two matter most: the second hold on the same resource and interval getting
409, and a test that writes a duplicate bucket row over a *separate connection* and
expects the unique-key violation — if that one ever passes silently, the guarantee
had moved back into the code.

1208 tests / 3495 assertions. phpstan at its 14-error baseline. Frozen slot contract
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:28:55 +03:30

9.5 KiB
Raw Blame History

چک‌لیست — تسک ۰۷ (رزرو موقت و ثبت نهایی چندمنبعی)

وضعیت کلی: بک‌اند، تضمین دیتابیسی و مستندات تکمیل (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 در UI رزرو UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI رزرو چندمنبعی
۴.۲ خطای 409 با پیام «این ساعت همین لحظه رزرو شد» + لیست جایگزین خودکار UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI رزرو چندمنبعی
۴.۳ خطای reschedule شامل «نوبت فعلی تغییری نکرد» UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI رزرو چندمنبعی
۴.۴ مسدودسازی موردی منبع از صفحهٔ منابع UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI رزرو چندمنبعی
۴.۵ تفکیک «مسدودسازی موردی» (occupancy) از «بلندمدت» (exception) در UI روشن است UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI رزرو چندمنبعی
۴.۶ هیچ رنگ/شعاع hard-code UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI رزرو چندمنبعی
۴.۷ دارک‌مود و حالت فشرده UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI رزرو چندمنبعی
۴.۸ RTL و موبایل UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI رزرو چندمنبعی
۴.۹ همهٔ رشته‌ها فارسی UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI رزرو چندمنبعی
۴.۱۰ AppointmentDetailPage بخش بخش‌های نوبت (فقط حالت resource) UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI رزرو چندمنبعی

۵. تست

# مورد وضعیت یادداشت
۵.۱ 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 .
۷.۱۱ موارد به‌تعویق با دلیل و تسک مقصد