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>
10 KiB
10 KiB
چکلیست — تسک ۱۳ (سیاست لغو، عدم حضور، لیست انتظار)
وضعیت کلی: ✅ تمامشده با انحرافهای ثبتشده · آخرین بازبینی: ۱۴۰۵/۰۵/۰۹
قواعد: _shared/definition-of-done.md · red-lines.md · ui-conventions.md
۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | --group=slot-mode-frozen سبز |
✅ | |
| ۰.۲ | پیشفرض سیاست بدون جریمه | ✅ | ⭐⭐ penaltyMode = 'none' در خودِ entity، نه در seed |
| ۰.۳ | بیمار پرریسک مسدود نمیشود | ✅ | ⭐ فقط برچسب؛ مسدودسازی = قانون eligibility تسک ۰۹ |
| ۰.۴ | is_reserve و صفحهاش دستنخورده |
✅ | مفهوم متفاوت؛ ادغام خارج از دامنه |
| ۰.۵ | وضعیتهای لغو موجود دستنخورده | ✅ | همان cancelled_by_* و no_show |
۱. بکاند — لغو و جریمه
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | CancellationPolicy · NoShowRecord |
✅ | |
| ۱.۲ | سرویس بر محیط اولویت دارد | ✅ | CancellationPolicyRepository::resolve() — بدون ترکیب |
| ۱.۳ | شرط «لغو توسط کلینیک» اولین خط | ✅ | ⭐ با کامنت توضیح چرا |
| ۱.۴ | سقف جریمه = مبلغ پرداختی | ✅ | |
| ۱.۵ | نوبت نقدی → جریمه صفر + note |
✅ | |
| ۱.۶ | GET /cancellation-preview |
✅ | ⭐ همان محاسبهٔ لغو واقعی |
| ۱.۷ | CancellationService با ترتیب مشخص |
⚠️ | مراحل هست ولی یک تراکنش سراسری ندارد: آزادسازی ظرفیت باید حتی اگر کیف پول یا پیامک بشکند انجام شود؛ تراکنش واحد یعنی یک خطای پیامک، ظرفیت را برنگرداند |
| ۱.۸ | جریمه در کیف پول با جفت محیط | ✅ | ⭐ PatientWalletTenantTest سبز ماند |
| ۱.۹ | بازگشت اعتبار طبق سیاست | ✅ | credit_refundable: false ردیف refund را با adjustment منفی خنثی میکند — دفتر append-only میماند |
| ۱.۱۰ | جلسهٔ دوره آزاد میشود | ✅ | از BookingService::cancel() که تسک ۱۲ وصلش کرد |
| ۱.۱۱ | لغو دوباره → ۴۰۹ | ✅ | |
| ۱.۱۲ | لغو نوبت گذشته → ۴۲۲ | ✅ |
۲. بکاند — عدم حضور
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | پنجرهٔ ۱۲ ماه | ✅ | NoShowRecordRepository::WINDOW_DAYS |
| ۲.۲ | برچسب از TenantTag موجود |
✅ | ⭐ هیچ ستون بولین تازهای |
| ۲.۳ | یک رکورد per نوبت | ✅ | کلید یکتا + بررسی پیش از درج |
| ۲.۴ | جدول جدا، نه شمارنده | ✅ | همان استدلال دفتر اعتبار تسک ۱۱ |
۳. بکاند — لیست انتظار
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | WaitlistEntry + WaitlistNotifier |
⚠️ | یک notifier بهجای دو کلاس Service/Matcher؛ تطبیق یک کوئری در repository است و کلاس جدا فقط لایه بود |
| ۳.۲ | broadcast به حداکثر ۱۰ نفر | ✅ | تصمیم و دلیلش در waitlist.md |
| ۳.۳ | جملهٔ «اولین نفر میبرد» در پیامک | ✅ | ⭐ |
| ۳.۴ | سقف notify_count |
✅ | ۳ بار |
| ۳.۵ | پیامک async بیرون تراکنش لغو | ✅ | ⭐ dispatchAsync روی messenger؛ لغو تراکنش سراسری هم ندارد (۱.۷) |
| ۳.۶ | ترتیب priority DESC, created_at ASC |
✅ | |
| ۳.۷ | فیلتر 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 |
۴. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | سه جدول | ✅ | Version20260731081142 |
| ۴.۲ | ایندکس تطبیق لیست انتظار | ✅ | |
| ۴.۳ | ایندکس پنجرهٔ عدم حضور | ✅ | |
| ۴.۴ | risk_tag_uuid بدون FK |
✅ | همان الگوی موجود پروژه |
| ۴.۵ | دستور seed سیاست پیشفرض | — | لازم نشد: نبودِ سیاست یعنی «بدون جریمه»، پس رفتار پیشفرض از قبل امن است |
| ۴.۶ | TenantSchemaCoverageTest سبز |
✅ |
۵. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | CancellationPolicyPage |
⚠️ | سیاست محیط کامل است؛ جدول override سرویسها ساخته نشد (اندپوینتش هست) |
| ۵.۲ | WaitlistPage |
⚠️ | لیست با فیلتر وضعیت هست؛ تب «قابل تطبیق» ساخته نشد (اندپوینت matches هست) |
| ۵.۳ | دکمهٔ لغو با محتوای preview | ✅ | ⭐ CancelAppointmentDialog جریمه و بازگشت اعتبار را پیش از تأیید نشان میدهد؛ سرویس لغو هم reason میگیرد و ردیف تایملاین مینویسد (قبلاً این مسیر هیچ ردی نمیگذاشت) |
| ۵.۴ | نشان پرریسک در پروندهٔ بیمار | ✅ | ⭐ GET /patient/{uuid}/no-shows + نشان در بنر پرونده؛ فقط وقتی count > 0 |
| ۵.۵ | ConfirmDialog موجود |
✅ | جای دیگری مودال دستساز ساخته نشد |
| ۵.۶ | فیلتر در URL | ✅ | useUrlState |
| ۵.۷ | تاریخ شمسی و مبلغ | ✅ | formatDate · PriceInput |
| ۵.۸ | backTo |
✅ | |
| ۵.۹ | هیچ رنگ hard-code | ✅ | |
| ۵.۱۰ | دارکمود و حالت فشرده | ✅ | اسکرینشات واقعی؛ دو ایراد پیدا و رفع شد (کارتِ بدون card-pad و برچسبِ داخل .field) |
| ۵.۱۱ | RTL و موبایل | ✅ | جدول لیست انتظار اسکرول افقی داخلی دارد |
| ۵.۱۲ | رشتهها فارسی | ✅ | |
| ۵.۱۳ | ReserveAppointmentsPage دستنخورده |
✅ |
۶. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | محاسبهٔ جریمه — پنج حالت | ✅ | ⭐ داخل پنجره، بیرون پنجره، کلینیک، سقف پرداختی، بدون سیاست |
| ۶.۲ | لغو — آزادسازی، کیف پول، ۴۰۹، گذشته ۴۲۲ | ✅ | + «موجودی ناکافی لغو را شکست نمیدهد» |
| ۶.۳ | اولویت سیاست سرویس بر محیط | ✅ | از راه عددِ خروجی سنجیده میشود نه از راه resolver |
| ۶.۴ | عدم حضور — سوم برچسب، دوبار یک رکورد | ✅ | ⭐ پنجرهٔ ۱۲ ماه تست نشد |
| ۶.۵ | بیمار پرریسک رزرو موفق دارد | ✅ | testATaggedPatientCanStillBook |
| ۶.۶ | لیست انتظار — ترتیب، سقف اطلاع، شعبه | ✅ | فیلتر روزبخش تست نشد (۳.۷) |
| ۶.۷ | تبدیل به رزرو | ✅ | تبدیل تنگ + idempotent، هر دو تست دارند |
| ۶.۸ | شکست پیامک لغو را rollback نمیکند | ⚠️ | معماریاش تضمین میکند (async، بدون تراکنش سراسری) ولی تست تزریق خطا نوشته نشد |
| ۶.۹ | تست کیف پول موجود سبز ماند | ✅ | ⭐ |
| ۶.۱۰ | سیاست اعتبار روی دوره | ✅ | credit_refundable: false با ردیف adjustment پس میگیرد؛ هیچ ردیفی حذف نمیشود |
اجرا: tests/Cancellation → ۱۴ تست · tests/Waitlist → ۹ تست.
۷. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | docs/api/cancellation.md |
✅ | دو قاعدهٔ گران با دلیلشان |
| ۷.۲ | docs/api/waitlist.md |
✅ | تصمیم broadcast و چرایی رد صف انحصاری |
۸. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۸.۱ | هیچ ⏳ بیدلیل نمانده | ✅ | ۱۲ مورد ⏳/⚠️ همه با دلیل و تسک مقصد |
| ۸.۲ | bin/phpunit کامل سبز |
✅ | flake پیدا و رفع شد: قید یکتای شکسته EntityManager را میبندد و کانتینر همان نمونهٔ بسته را برمیگرداند، پس تستِ بعدی مدیرِ مرده را ارث میبرد. setUp حالا ریست میکند و UserCollisionRetryTest آن را پین میکند. سه اجرای پیاپی سبز |
| ۸.۳ | --group=slot-mode-frozen سبز |
✅ | |
| ۸.۴ | phpstan بدون خطای جدید |
✅ | ۱۴ = baseline |
| ۸.۵ | npx tsc --noEmit و تستهای فرانت سبز |
✅ | ۶۳۲ تست |
| ۸.۶ | تستهای tenant سبز | ✅ | |
| ۸.۷ | docs/api/* بهروز |
✅ | |
| ۸.۸ | چکلیست UI کامل | ⚠️ | جز ۵.۱ (جدول override) و ۵.۲ (تب «قابل تطبیق») |
| ۸.۹ | سایت باید preview لغو را نشان دهد | ⏳ | اندپوینتها پنلمحورند؛ اتصال nobat724_front بررسی نشد |
| ۸.۱۰ | clinic-pro-tauri بررسی شد |
⏳ | همان |
| ۸.۱۱ | commit، سپس graphify update . |
✅ | دو کامیت جدا |
| ۸.۱۲ | موارد بهتعویق با دلیل | ✅ | روزبخش/تبدیل/انقضا (۳.۷–۳.۹) و رویدادها → تسک ۱۴ · اتصال UI لغو (۵.۳) و نشان پرریسک (۵.۴) · flake تست (۸.۲) |