perf(reports): read every resource's calendar in one batch, and close the owed tests

Writing the query-count test that task 14 owed showed the growth was real: one
resource cost 10 queries, six cost 33 — about five per resource, because the
available-minutes figure walked each resource's calendar on its own.

Holidays, tenant overrides and branch hours are identical for every resource in
a report, so they now load once outside the loop; shifts and exceptions load for
all resources in one query each. The batched path is a new method rather than a
change to rawAvailability, which the booking engine also calls. The test pins
the shape of the growth, not an exact count.

Also landed:

- app:segment:seed-templates with beauty, dental and physio presets. Building
  four segments and their requirements by hand is the first thing a new clinic
  must do and the most tedious; this gives them something to edit instead of an
  empty page. It refuses to touch a service that already has segments unless
  --force, and it will not invent resource types the tenant never defined.
- book-all is all-or-nothing, proven rather than asserted: with a calendar open
  one day a week and a 1-2 day protocol gap, session one finds a slot and
  session two cannot, and every session must come back planned.
- credit_refundable: false takes the credit back with a negative adjustment and
  deletes nothing — the ledger stays append-only.
- the segments editor has frontend tests, including that it sends back what the
  user sees and renders read-only without the permission.

useBranches now returns [] for a non-array payload instead of throwing
"branches.map is not a function" and taking the page down with it.

BookingLocationsScanTest built a Clinic around a Doctor loaded from a different
manager, which Doctrine treats as a new entity; it flushed fine most runs and
failed on cascade in others. It now loads the doctor from the same manager.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-01 14:22:22 +03:30
co-authored by Claude Opus 5
parent 92edd175cc
commit b3c331f0cb
20 changed files with 624 additions and 30 deletions
@@ -53,10 +53,10 @@
| ۳.۴ | سقف `notify_count` | ✅ | ۳ بار |
| ۳.۵ | پیامک async بیرون تراکنش لغو | ✅ | ⭐ `dispatchAsync` روی messenger؛ لغو تراکنش سراسری هم ندارد (۱.۷) |
| ۳.۶ | ترتیب `priority DESC, created_at ASC` | ✅ | |
| ۳.۷ | فیلتر `preferred_day_parts` | | ذخیره و نمایش می‌شود ولی در تطبیق اعمال نمی‌شود — بدون منطقهٔ زمانی شعبه، «عصر» تعریف قطعی ندارد؛ به تسک ۱۴ موکول شد |
| ۳.۸ | `converted` خودکار روی رزرو بیمار | | نیازمند رویداد `AppointmentBooked` که تسک ۱۴ می‌سازد |
| ۳.۹ | `app:waitlist:expire` روزانه | | ردیف منقضی در تطبیق نمی‌آید (`desiredTo >= now`)، پس اثر عملی ندارد؛ پاکسازی با تسک ۱۴ |
| ۳.۱۰ | بازهٔ بیش از ۹۰ روز → ۴۲۲ | | فقط بازهٔ گذشته و وارونه رد می‌شود |
| ۳.۷ | فیلتر `preferred_day_parts` | | ⭐ مرزها در `WaitlistEntry::DAY_PARTS` با ساعت **محلی شعبه**؛ فیلتر پیش از بریدن به ده نفر اعمال می‌شود |
| ۳.۸ | `converted` خودکار روی رزرو بیمار | | ⭐ از رویداد `AppointmentBooked`، نه از داخل `BookingService` — تبدیل نباید بتواند نوبت واقعی را rollback کند |
| ۳.۹ | `app:waitlist:expire` روزانه | | سرویس + دستور + پیام روزانهٔ زمان‌بند؛ وضعیت عوض می‌شود نه حذف |
| ۳.۱۰ | بازهٔ بیش از ۹۰ روز → ۴۲۲ | | همان افق رزرو تسک ۱۲ |
| ۳.۱۱ | هفت endpoint | ✅ | ۱۰ تا: سیاست GET/PUT + override + preview + cancel + no-show + لیست انتظار GET/POST/DELETE/matches |
## ۴. دیتابیس
@@ -67,7 +67,7 @@
| ۴.۲ | ایندکس تطبیق لیست انتظار | ✅ | |
| ۴.۳ | ایندکس پنجرهٔ عدم حضور | ✅ | |
| ۴.۴ | `risk_tag_uuid` بدون FK | ✅ | همان الگوی موجود پروژه |
| ۴.۵ | دستور seed سیاست پیش‌فرض | | لازم نشد: نبودِ سیاست یعنی «بدون جریمه»، پس رفتار پیش‌فرض از قبل امن است |
| ۴.۵ | دستور seed سیاست پیش‌فرض | | لازم نشد: نبودِ سیاست یعنی «بدون جریمه»، پس رفتار پیش‌فرض از قبل امن است |
| ۴.۶ | `TenantSchemaCoverageTest` سبز | ✅ | |
## ۵. UI
@@ -77,7 +77,7 @@
| ۵.۱ | `CancellationPolicyPage` | ⚠️ | سیاست محیط کامل است؛ جدول override سرویس‌ها ساخته نشد (اندپوینتش هست) |
| ۵.۲ | `WaitlistPage` | ⚠️ | لیست با فیلتر وضعیت هست؛ تب «قابل تطبیق» ساخته نشد (اندپوینت `matches` هست) |
| ۵.۳ | دکمهٔ لغو با محتوای preview | ✅ | ⭐ `CancelAppointmentDialog` جریمه و بازگشت اعتبار را **پیش از** تأیید نشان می‌دهد؛ سرویس لغو هم `reason` می‌گیرد و ردیف تایم‌لاین می‌نویسد (قبلاً این مسیر هیچ ردی نمی‌گذاشت) |
| ۵.۴ | نشان پرریسک در پروندهٔ بیمار | | برچسب از `TenantTag` می‌آید و در پرونده دیده می‌شود، ولی شمارش عدم حضور نمایش داده نمی‌شود |
| ۵.۴ | نشان پرریسک در پروندهٔ بیمار | | `GET /patient/{uuid}/no-shows` + نشان در بنر پرونده؛ فقط وقتی `count > 0` |
| ۵.۵ | `ConfirmDialog` موجود | ✅ | جای دیگری مودال دست‌ساز ساخته نشد |
| ۵.۶ | فیلتر در URL | ✅ | `useUrlState` |
| ۵.۷ | تاریخ شمسی و مبلغ | ✅ | `formatDate` · `PriceInput` |
@@ -94,14 +94,14 @@
|---|---|---|---|
| ۶.۱ | محاسبهٔ جریمه — پنج حالت | ✅ | ⭐ داخل پنجره، بیرون پنجره، کلینیک، سقف پرداختی، بدون سیاست |
| ۶.۲ | لغو — آزادسازی، کیف پول، ۴۰۹، گذشته ۴۲۲ | ✅ | + «موجودی ناکافی لغو را شکست نمی‌دهد» |
| ۶.۳ | اولویت سیاست سرویس بر محیط | | `resolve()` نوشته شد ولی تست اختصاصی ندارد |
| ۶.۳ | اولویت سیاست سرویس بر محیط | | از راه عددِ خروجی سنجیده می‌شود نه از راه resolver |
| ۶.۴ | عدم حضور — سوم برچسب، دوبار یک رکورد | ✅ | ⭐ پنجرهٔ ۱۲ ماه تست نشد |
| ۶.۵ | بیمار پرریسک رزرو موفق دارد | | برچسب هیچ‌جا بررسی نمی‌شود، پس مسدودسازی ممکن نیست |
| ۶.۵ | بیمار پرریسک رزرو موفق دارد | | `testATaggedPatientCanStillBook` |
| ۶.۶ | لیست انتظار — ترتیب، سقف اطلاع، شعبه | ✅ | فیلتر روزبخش تست نشد (۳.۷) |
| ۶.۷ | تبدیل به رزرو | | با ۳.۸ |
| ۶.۷ | تبدیل به رزرو | | تبدیل تنگ + idempotent، هر دو تست دارند |
| ۶.۸ | شکست پیامک لغو را rollback نمی‌کند | ⚠️ | معماری‌اش تضمین می‌کند (async، بدون تراکنش سراسری) ولی تست تزریق خطا نوشته نشد |
| ۶.۹ | تست کیف پول موجود سبز ماند | ✅ | ⭐ |
| ۶.۱۰ | سیاست اعتبار روی دوره | | مسیرش هست (`credit_refundable`)، تست ترکیبی با دوره نوشته نشد |
| ۶.۱۰ | سیاست اعتبار روی دوره | | `credit_refundable: false` با ردیف `adjustment` پس می‌گیرد؛ هیچ ردیفی حذف نمی‌شود |
**اجرا:** `tests/Cancellation` → ۱۴ تست · `tests/Waitlist` → ۹ تست.
@@ -123,7 +123,7 @@
| ۸.۵ | `npx tsc --noEmit` و تست‌های فرانت سبز | ✅ | ۶۳۲ تست |
| ۸.۶ | تست‌های tenant سبز | ✅ | |
| ۸.۷ | `docs/api/*` به‌روز | ✅ | |
| ۸.۸ | چک‌لیست UI کامل | ⚠️ | جز ۵.۱، ۵.۲، ۵ |
| ۸.۸ | چک‌لیست UI کامل | ⚠️ | جز ۵.۱ (جدول override) و ۵.۲ (تب «قابل تطبیق») |
| ۸.۹ | سایت باید preview لغو را نشان دهد | ⏳ | اندپوینت‌ها پنل‌محورند؛ اتصال `nobat724_front` بررسی نشد |
| ۸.۱۰ | `clinic-pro-tauri` بررسی شد | ⏳ | همان |
| ۸.۱۱ | commit، سپس `graphify update .` | ✅ | دو کامیت جدا |