Files
clinicpro/docs/new_feture/taskes/task-13-cancellation-waitlist/checklist.md
T
hamedandClaude Opus 5 d98a0396a4 test(policy): fail if the schema advertises a field nothing ever supplies
Task 09 left this as its starred risk and deferred it to task 10, which then
shipped without it. The failure mode is silent and expensive: an operator
writes a rule on a field no call site puts in the context, activates it, and it
never matches — no error, no log, and the clinic believes the rule is running.

The test is structural rather than behavioural on purpose. Walking every real
path for every field would need a test rig larger than the engine; asserting
that each advertised field is populated somewhere in src/ catches the case that
actually happens, which is a field added to the schema and nowhere else.

Also closes the last few rows that had gone stale:

- evaluateIsolated: PolicyResolver::evaluateOne() landed with the sandbox
- forbid before candidate generation: the plan builder already reads
  prohibitions before the availability engine is reached
- appointments.applied_policies and app:policy:seed-examples are declined with
  their reasons rather than left open — the trace lives on the price snapshot
  and a second column would be a second source of truth, and the template
  registry does the seeding job from inside the UI where the user can see the
  result before creating anything
- the reserve list keeps its page in the URL like every other panel list

Every checklist across the sixteen tasks now has zero pending rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 14:37:32 +03:30

11 KiB
Raw Blame History

چک‌لیست — تسک ۱۳ (سیاست لغو، عدم حضور، لیست انتظار)

وضعیت کلی: تمام‌شده با انحراف‌های ثبت‌شده · آخرین بازبینی: ۱۴۰۵/۰۵/۰۹

قواعد: _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 لغو را نشان دهد مودال لغو سایت قبلاً هیچ درخواستی نمی‌فرستاد؛ حالا /cancel را با پیش‌نمایش جریمه صدا می‌زند
۸.۱۰ clinic-pro-tauri بررسی شد هیچ اندپوینت نوبتی را مصرف نمی‌کند؛ صفحه‌های src/components/turns/ روی src/data/detail.json (ماک ثابت) کار می‌کنند، پس انحرافی ممکن نیست
۸.۱۱ commit، سپس graphify update . دو کامیت جدا
۸.۱۲ موارد به‌تعویق با دلیل روزبخش/تبدیل/انقضا (۳.۷–۳.۹) و رویدادها → تسک ۱۴ · اتصال UI لغو (۵.۳) و نشان پرریسک (۵.۴) · flake تست (۸.۲)