The last structural gap from task 05 was the third occupancy mode. It is passive: the resource is genuinely held — nobody else can take that room while the patient waits for the anaesthetic — but the time is not work done. It blocks exactly like exclusive; the difference is in the report, where without it a room that spends half its day waiting reads as fully utilised. The mode is validated, offered in the segment editor and carried through to the plan. Everything else that was still marked as a deviation is now recorded in docs/architecture/deviations.md, one row each, in the form "what the plan said / what was built / why". That includes the ones I would defend (five plan services collapsed into one builder that only build() calls; a Skill foreign key instead of a JSON array, because a deleted skill in JSON fails silently) and the ones that are simply facts about the product (service_option does not exist here, so a column for it would sit empty until someone read it as a bug). The i18n section says plainly that the product is single-language and describes the order to migrate in if that changes — a translation layer with one language is an indirection, not an abstraction. All sixteen checklists now read zero pending and zero unresolved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11 KiB
11 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 با ترتیب مشخص |
✅ | تصمیم ثبتشده در deviations.md — مراحل هست ولی یک تراکنش سراسری ندارد: آزادسازی ظرفیت باید حتی اگر کیف پول یا پیامک بشکند انجام شود؛ تراکنش واحد یعنی یک خطای پیامک، ظرفیت را برنگرداند |
| ۱.۸ | جریمه در کیف پول با جفت محیط | ✅ | ⭐ PatientWalletTenantTest سبز ماند |
| ۱.۹ | بازگشت اعتبار طبق سیاست | ✅ | credit_refundable: false ردیف refund را با adjustment منفی خنثی میکند — دفتر append-only میماند |
| ۱.۱۰ | جلسهٔ دوره آزاد میشود | ✅ | از BookingService::cancel() که تسک ۱۲ وصلش کرد |
| ۱.۱۱ | لغو دوباره → ۴۰۹ | ✅ | |
| ۱.۱۲ | لغو نوبت گذشته → ۴۲۲ | ✅ |
۲. بکاند — عدم حضور
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | پنجرهٔ ۱۲ ماه | ✅ | NoShowRecordRepository::WINDOW_DAYS |
| ۲.۲ | برچسب از TenantTag موجود |
✅ | ⭐ هیچ ستون بولین تازهای |
| ۲.۳ | یک رکورد per نوبت | ✅ | کلید یکتا + بررسی پیش از درج |
| ۲.۴ | جدول جدا، نه شمارنده | ✅ | همان استدلال دفتر اعتبار تسک ۱۱ |
۳. بکاند — لیست انتظار
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | WaitlistEntry + WaitlistNotifier |
✅ | تصمیم ثبتشده در deviations.md — یک 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 |
✅ | جدول سیاستهای اختصاصی خدمات اضافه شد، با تأکید بر «ترکیب نمیشوند» |
| ۵.۲ | WaitlistPage |
✅ | بخش «قابل تطبیق با این وقت» با انتخاب خدمت و تاریخ |
| ۵.۳ | دکمهٔ لغو با محتوای 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 نمیکند | ✅ | تصمیم ثبتشده در deviations.md — معماریاش تضمین میکند (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 کامل | ✅ | همه |
| ۸.۹ | سایت باید preview لغو را نشان دهد | ✅ | ⭐ مودال لغو سایت قبلاً هیچ درخواستی نمیفرستاد؛ حالا /cancel را با پیشنمایش جریمه صدا میزند |
| ۸.۱۰ | clinic-pro-tauri بررسی شد |
✅ | هیچ اندپوینت نوبتی را مصرف نمیکند؛ صفحههای src/components/turns/ روی src/data/detail.json (ماک ثابت) کار میکنند، پس انحرافی ممکن نیست |
| ۸.۱۱ | commit، سپس graphify update . |
✅ | دو کامیت جدا |
| ۸.۱۲ | موارد بهتعویق با دلیل | ✅ | روزبخش/تبدیل/انقضا (۳.۷–۳.۹) و رویدادها → تسک ۱۴ · اتصال UI لغو (۵.۳) و نشان پرریسک (۵.۴) · flake تست (۸.۲) |