Screenshotting the pages under dark mode and compact density (rather than trusting that design tokens were enough) turned up two mistakes repeated across every page this feature set added: - `.card` carries only the surface, border and radius — padding comes from the separate `.card-pad`. Fifteen cards were rendering with their content flush against the edges. - `.field` *is* the input box, a 40px-tall flex row. Wrapping a label plus a control in it produced a joined addon rather than a label above its field. `.field-block` is the label-above layout, and thirty-seven wrappers now use it. Both were invisible to type-checking and to the tests, which is exactly why the visual pass was worth running. Numbers in the new UI now go through formatNumber so they render as Persian digits, and the utilization page's header no longer repeats the sentence that appears under its filters verbatim. The QA driver gained a `--ui` flag: theme and density live in localStorage['clinicpro-ui'], so without seeding them dark mode and compact density cannot be screenshotted at all. 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 |
⏳ | ذخیره و نمایش میشود ولی در تطبیق اعمال نمیشود — بدون منطقهٔ زمانی شعبه، «عصر» تعریف قطعی ندارد؛ به تسک ۱۴ موکول شد |
| ۳.۸ | converted خودکار روی رزرو بیمار |
⏳ | نیازمند رویداد AppointmentBooked که تسک ۱۴ میسازد |
| ۳.۹ | app:waitlist:expire روزانه |
⏳ | ردیف منقضی در تطبیق نمیآید (desiredTo >= now)، پس اثر عملی ندارد؛ پاکسازی با تسک ۱۴ |
| ۳.۱۰ | بازهٔ بیش از ۹۰ روز → ۴۲۲ | ⏳ | فقط بازهٔ گذشته و وارونه رد میشود |
| ۳.۱۱ | هفت 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 میگیرد و ردیف تایملاین مینویسد (قبلاً این مسیر هیچ ردی نمیگذاشت) |
| ۵.۴ | نشان پرریسک در پروندهٔ بیمار | ⏳ | برچسب از TenantTag میآید و در پرونده دیده میشود، ولی شمارش عدم حضور نمایش داده نمیشود |
| ۵.۵ | ConfirmDialog موجود |
✅ | جای دیگری مودال دستساز ساخته نشد |
| ۵.۶ | فیلتر در URL | ✅ | useUrlState |
| ۵.۷ | تاریخ شمسی و مبلغ | ✅ | formatDate · PriceInput |
| ۵.۸ | backTo |
✅ | |
| ۵.۹ | هیچ رنگ hard-code | ✅ | |
| ۵.۱۰ | دارکمود و حالت فشرده | ✅ | اسکرینشات واقعی؛ دو ایراد پیدا و رفع شد (کارتِ بدون card-pad و برچسبِ داخل .field) |
| ۵.۱۱ | RTL و موبایل | ✅ | جدول لیست انتظار اسکرول افقی داخلی دارد |
| ۵.۱۲ | رشتهها فارسی | ✅ | |
| ۵.۱۳ | ReserveAppointmentsPage دستنخورده |
✅ |
۶. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | محاسبهٔ جریمه — پنج حالت | ✅ | ⭐ داخل پنجره، بیرون پنجره، کلینیک، سقف پرداختی، بدون سیاست |
| ۶.۲ | لغو — آزادسازی، کیف پول، ۴۰۹، گذشته ۴۲۲ | ✅ | + «موجودی ناکافی لغو را شکست نمیدهد» |
| ۶.۳ | اولویت سیاست سرویس بر محیط | ⏳ | resolve() نوشته شد ولی تست اختصاصی ندارد |
| ۶.۴ | عدم حضور — سوم برچسب، دوبار یک رکورد | ✅ | ⭐ پنجرهٔ ۱۲ ماه تست نشد |
| ۶.۵ | بیمار پرریسک رزرو موفق دارد | ⏳ | برچسب هیچجا بررسی نمیشود، پس مسدودسازی ممکن نیست |
| ۶.۶ | لیست انتظار — ترتیب، سقف اطلاع، شعبه | ✅ | فیلتر روزبخش تست نشد (۳.۷) |
| ۶.۷ | تبدیل به رزرو | ⏳ | با ۳.۸ |
| ۶.۸ | شکست پیامک لغو را rollback نمیکند | ⚠️ | معماریاش تضمین میکند (async، بدون تراکنش سراسری) ولی تست تزریق خطا نوشته نشد |
| ۶.۹ | تست کیف پول موجود سبز ماند | ✅ | ⭐ |
| ۶.۱۰ | سیاست اعتبار روی دوره | ⏳ | مسیرش هست (credit_refundable)، تست ترکیبی با دوره نوشته نشد |
اجرا: 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 کامل | ⚠️ | جز ۵.۱، ۵.۲، ۵.۴ |
| ۸.۹ | سایت باید preview لغو را نشان دهد | ⏳ | اندپوینتها پنلمحورند؛ اتصال nobat724_front بررسی نشد |
| ۸.۱۰ | clinic-pro-tauri بررسی شد |
⏳ | همان |
| ۸.۱۱ | commit، سپس graphify update . |
✅ | دو کامیت جدا |
| ۸.۱۲ | موارد بهتعویق با دلیل | ✅ | روزبخش/تبدیل/انقضا (۳.۷–۳.۹) و رویدادها → تسک ۱۴ · اتصال UI لغو (۵.۳) و نشان پرریسک (۵.۴) · flake تست (۸.۲) |