Cancelling worked but had no policy behind it: no window, no penalty, nothing happened to the deposit, and the no_show status had no effect at all. Two rules that are expensive to get wrong, and both are load-bearing: - The clinic cancelling its own appointment is never charged. That check is the first line of the calculation, not somewhere in the middle, so a later refactor cannot reorder it into charging patients for the clinic's decision. - A penalty never exceeds what was actually paid. Anything above that is a debt, and debt belongs to billing, not to cancellation. An unpaid appointment is charged nothing and the response says why. The default is no penalty at all — a penalising default would have made every patient with a near appointment liable the moment this deployed. No-shows are rows, not a counter on the patient: a counter loses which appointment and when, which makes the 12-month window impossible. Crossing the threshold adds an existing TenantTag; it never blocks the patient, because blocking is an eligibility policy (task 09) written on top of that same tag. Waitlist notifies up to ten matching people and the first to book wins. An exclusive queue reads fairer but means a freed slot sits locked for half an hour while someone ignores their phone — so the SMS says so explicitly instead. Insufficient wallet balance does not fail the cancellation: the slot is freed either way. A slot should not be held hostage to money. 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 | ⏳ | هوک useCancellationPreview و useCancelAppointment آمادهاند؛ اتصال به AppointmentDetailPage انجام نشد |
| ۵.۴ | نشان پرریسک در پروندهٔ بیمار | ⏳ | برچسب از TenantTag میآید و در پرونده دیده میشود، ولی شمارش عدم حضور نمایش داده نمیشود |
| ۵.۵ | ConfirmDialog موجود |
✅ | جای دیگری مودال دستساز ساخته نشد |
| ۵.۶ | فیلتر در URL | ✅ | useUrlState |
| ۵.۷ | تاریخ شمسی و مبلغ | ✅ | formatDate · PriceInput |
| ۵.۸ | backTo |
✅ | |
| ۵.۹ | هیچ رنگ hard-code | ✅ | |
| ۵.۱۰ | دارکمود و حالت فشرده | ⚠️ | فقط توکنها؛ بازبینی چشمی انجام نشد |
| ۵.۱۱ | RTL و موبایل | ✅ | جدول لیست انتظار اسکرول افقی داخلی دارد |
| ۵.۱۲ | رشتهها فارسی | ✅ | |
| ۵.۱۳ | ReserveAppointmentsPage دستنخورده |
✅ |
۶. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | محاسبهٔ جریمه — پنج حالت | ✅ | ⭐ داخل پنجره، بیرون پنجره، کلینیک، سقف پرداختی، بدون سیاست |
| ۶.۲ | لغو — آزادسازی، کیف پول، ۴۰۹، گذشته ۴۲۲ | ✅ | + «موجودی ناکافی لغو را شکست نمیدهد» |
| ۶.۳ | اولویت سیاست سرویس بر محیط | ⏳ | resolve() نوشته شد ولی تست اختصاصی ندارد |
| ۶.۴ | عدم حضور — سوم برچسب، دوبار یک رکورد | ✅ | ⭐ پنجرهٔ ۱۲ ماه تست نشد |
| ۶.۵ | بیمار پرریسک رزرو موفق دارد | ⏳ | برچسب هیچجا بررسی نمیشود، پس مسدودسازی ممکن نیست |
| ۶.۶ | لیست انتظار — ترتیب، سقف اطلاع، شعبه | ✅ | فیلتر روزبخش تست نشد (۳.۷) |
| ۶.۷ | تبدیل به رزرو | ⏳ | با ۳.۸ |
| ۶.۸ | شکست پیامک لغو را rollback نمیکند | ⚠️ | معماریاش تضمین میکند (async، بدون تراکنش سراسری) ولی تست تزریق خطا نوشته نشد |
| ۶.۹ | تست کیف پول موجود سبز ماند | ✅ | ⭐ |
| ۶.۱۰ | سیاست اعتبار روی دوره | ⏳ | مسیرش هست (credit_refundable)، تست ترکیبی با دوره نوشته نشد |
اجرا: tests/Cancellation → ۱۴ تست · tests/Waitlist → ۹ تست.
۷. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | docs/api/cancellation.md |
✅ | دو قاعدهٔ گران با دلیلشان |
| ۷.۲ | docs/api/waitlist.md |
✅ | تصمیم broadcast و چرایی رد صف انحصاری |
۸. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۸.۱ | هیچ ⏳ بیدلیل نمانده | ✅ | ۱۲ مورد ⏳/⚠️ همه با دلیل و تسک مقصد |
| ۸.۲ | bin/phpunit کامل سبز |
⚠️ | ۱۳۰۵ تست سبز است، ولی حدود ۴۰٪ اجراهای کامل یک خطای EntityManager is closed روی یک تست تصادفیِ نامرتبط میدهند. در اجرای زیرمجموعهها هرگز تکرار نمیشود و تست خطاده هر بار عوض میشود. با حذف تستهای این تسک هم دیده شد ⇒ احتمالاً flake محیط ddev، نه رگرسیون این تسک. باید جدا بررسی شود |
| ۸.۳ | --group=slot-mode-frozen سبز |
✅ | |
| ۸.۴ | phpstan بدون خطای جدید |
✅ | ۱۴ = baseline |
| ۸.۵ | npx tsc --noEmit و تستهای فرانت سبز |
✅ | ۶۳۲ تست |
| ۸.۶ | تستهای tenant سبز | ✅ | |
| ۸.۷ | docs/api/* بهروز |
✅ | |
| ۸.۸ | چکلیست UI کامل | ⚠️ | جز ۵.۱، ۵.۲، ۵.۳، ۵.۴، ۵.۱۰ |
| ۸.۹ | سایت باید preview لغو را نشان دهد | ⏳ | اندپوینتها پنلمحورند؛ اتصال nobat724_front بررسی نشد |
| ۸.۱۰ | clinic-pro-tauri بررسی شد |
⏳ | همان |
| ۸.۱۱ | commit، سپس graphify update . |
✅ | دو کامیت جدا |
| ۸.۱۲ | موارد بهتعویق با دلیل | ✅ | روزبخش/تبدیل/انقضا (۳.۷–۳.۹) و رویدادها → تسک ۱۴ · اتصال UI لغو (۵.۳) و نشان پرریسک (۵.۴) · flake تست (۸.۲) |