diff --git a/assets/admin/components/ServiceSegmentsTab.tsx b/assets/admin/components/ServiceSegmentsTab.tsx index 4c6ecc67..b4c564ce 100644 --- a/assets/admin/components/ServiceSegmentsTab.tsx +++ b/assets/admin/components/ServiceSegmentsTab.tsx @@ -11,7 +11,7 @@ interface SegmentRequirementDraft { type_uuid: string; skill_uuid: string | null; count: number; - occupancy: 'exclusive' | 'shared'; + occupancy: 'exclusive' | 'shared' | 'passive'; constraints: string[]; } @@ -48,6 +48,8 @@ interface ResourceType { const OCCUPANCY_OPTIONS = [ { value: 'exclusive', label: 'انحصاری — منبع کامل قفل می‌شود' }, { value: 'shared', label: 'اشتراکی — از ظرفیت منبع یکی کم می‌شود' }, + // مثل انحصاری قفل می‌کند، ولی در گزارش «کار مفید» حساب نمی‌شود. + { value: 'passive', label: 'رزرو بدون کار — اتاق اشغال است ولی کاری انجام نمی‌شود' }, ]; const DURATION_OPTIONS = [ @@ -280,7 +282,7 @@ export default function ServiceSegmentsTab({ serviceUuid, canEdit }: Props) { value={requirement.occupancy} onChange={(v) => patchRequirement(index, reqIndex, { - occupancy: (v as 'exclusive' | 'shared') ?? 'exclusive', + occupancy: (v as 'exclusive' | 'shared' | 'passive') ?? 'exclusive', }) } options={OCCUPANCY_OPTIONS} diff --git a/docs/architecture/deviations.md b/docs/architecture/deviations.md new file mode 100644 index 00000000..c3b31176 --- /dev/null +++ b/docs/architecture/deviations.md @@ -0,0 +1,89 @@ +# انحراف‌ها از نقشهٔ تسک‌ها + +هر ردیفی که در چک‌لیست‌ها ⚠️ مانده، اینجا با سه چیز ثبت است: **نقشه چه گفت**، **چه شد**، +و **چرا**. هیچ‌کدام «انجام نشده» نیست؛ همه تصمیم‌اند. + +قاعدهٔ خواندن: اگر روزی یکی از این تصمیم‌ها اشتباه از آب درآمد، همین‌جا عوضش کن و دلیل +تازه را بنویس. سندی که فقط توجیه جمع کند به درد نمی‌خورد. + +--- + +## چیزهایی که **نباید** به نقشه برگردند + +هیچ‌کدام. سه موردی که قبلاً اینجا بودند (idempotency با `catch`، تراکنش سراسری لغو، و +مانده تجمعی در UI) به نقشه برگردانده شدند — هر کدام با بستنِ باگی که دعوتش می‌کردند. +جزئیات در کامیت `refactor: take the three risky rows back to the plan`. + +--- + +## تسک ۰۴ — کاتالوگ سرویس + +| ردیف | نقشه | چه شد | چرا | +|---|---|---|---| +| ۴.۳ | جدول آیتم‌ها با نام/زمان/قیمت | جدول در **پیش‌نمایش انتخاب** است | همان ستون‌ها، ولی کنار عددی که کاربر دربارهٔ آن تصمیم می‌گیرد؛ جدول دوم یعنی دو جای خواندن یک چیز | +| ۴.۶ | قیمت با `PriceInput` | فقط نمایش با `formatRial` | این تب قیمت را **ویرایش نمی‌کند**؛ ویرایشش در `ServiceItemFormModal` است که از قبل `PriceInput` دارد | +| ۴.۱۰ | فرم با RHF + Zod | ذخیرهٔ inline per تغییر | RHF برای فرمی است که دکمهٔ submit دارد. این تب ندارد — هر تغییر مستقل و فوری ذخیره می‌شود | +| ۴.۱۲ | خطاها زیر همان گروه | همهٔ خطاها زیر پیش‌نمایش | قرارداد پروژه «همهٔ خطاها با هم» است؛ نگاشت `group_uuid` به کارت، خطا را از کنار عددِ نتیجه دور می‌کرد | + +## تسک ۰۵ — برنامهٔ نوبت + +| ردیف | نقشه | چه شد | چرا | +|---|---|---|---| +| ۱.۳ | پنج سرویس جدا | یک `AppointmentPlanBuilder` | پنج کلاس وقتی معنا دارد که هرکدام مصرف‌کنندهٔ مستقل داشته باشند؛ اینجا هر پنج فقط از `build()` صدا زده می‌شوند | +| ۱.۴ | `build()` تابع خالص | از دیتابیس می‌خواند | تا تسک ۰۸ خالص بود؛ تسک ۰۹ قوانین را وصل کرد و قانون در دیتابیس است. خلوص را فقط با کپی‌کردن قوانین در حافظه می‌شد نگه داشت | +| ۱.۸ | اشغال جدا از offset نمایشی | `setup/cleanup` روی `PlannedRequirement` | اثر عملی یکی است و تسک ۰۷ همان را می‌خواند؛ دو offset جدا یعنی دو عدد که باید هم‌زمان درست بمانند | +| ۲.۲ | `service_item_id` یا `service_option_id` | فقط `ServiceItem` | مفهوم `service_option` در این محصول وجود ندارد. ستونی برای چیزی که نیست، تهی می‌ماند و بعداً کسی فکر می‌کند باگ است | +| ۲.۳ | `fixed_minutes` یا `duration_share` | `duration_source` ∈ `fixed`\|`items` | «مدت از آیتم‌ها» همان نیاز واقعی است؛ سهم درصدی هیچ مصرف‌کننده‌ای نداشت | +| ۲.۵ | `required_skills` به‌صورت JSON | یک `Skill` با FK | چند مهارت هم‌زمان نیاز واقعی نداشت، و FK اعتبار ارجاعی می‌دهد که JSON نمی‌دهد — مهارتِ حذف‌شده در JSON بی‌صدا می‌ماند | + +## تسک ۰۹ — موتور قوانین + +رجیستری‌ها، شش موتور و `specificity` ذخیره‌شده **ساخته شدند** (کامیت +`refactor(policy): build the registries and six engines`). آنچه مانده: + +| ردیف | نقشه | چه شد | چرا | +|---|---|---|---| +| ۱.۱۱ | `forbiddenRanges()` — کوئری نه حلقه | `spacing` در لحظهٔ رزرو موقت | هزینه‌اش یک اسلات است که نمایش داده و بعد رد می‌شود؛ سودش این است که جستجوی وقت per کاندید یک کوئری تاریخچهٔ بیمار نزند | +| ۲.۳ | تسک ۰۶ → `Spacing` | `BookingPolicyGuard` | همان نتیجه، در همان نقطه‌ای که تصمیم واقعی گرفته می‌شود | +| ۲.۴ | `Eligibility` در `confirm` | در `hold` | رد کردن **بعد از** گرفتن صندلی هم وقت بیمار را تلف می‌کند هم صندلی را | +| ۲.۶ | هیچ امضایی عوض نشد | سه سرویس یک وابستگی سازنده گرفتند | امضای هیچ متد عمومی‌ای عوض نشد؛ سازنده تنها راه رسیدن قانون به آن سه است | +| نام فیلدها | `patient.age` نقطه‌دار | `patient_age` | شرط‌های ذخیره‌شده روی قانون‌های **زندهٔ کلینیک‌ها** به نام‌های فعلی اشاره می‌کنند؛ تغییرشان مهاجرت داده است و نگاشت یک‌به‌یک هم ندارد | + +## تسک ۱۰ — آزمایشگاه قانون + +| ردیف | نقشه | چه شد | چرا | +|---|---|---|---| +| ۱.۶ | فیلتر از داخل `condition` | از **دامنهٔ** قانون | شرط‌ها فیلد id ندارند که به کوئری ترجمه شوند؛ دامنه دقیقاً همان چیزی است که قابل ترجمه است | + +## تسک ۱۱ — دفتر اعتبار + +| ردیف | نقشه | چه شد | چرا | +|---|---|---|---| +| ۴.۴ | مانده هرگز منفی نمی‌شود | با مصرف پشت‌سرهم تست شد | تست هم‌زمانی واقعی حالا هست (`testAConcurrentConsumeIsAbsorbedWithoutBurningTheRequest`) | + +## تسک ۱۲ — دورهٔ درمان + +| ردیف | نقشه | چه شد | چرا | +|---|---|---|---| +| ۱.۱۲ | سخت‌گیرانه‌تر برنده در هر دو جهت | فقط `max(min)` | قانون `spacing` اثر «حداکثر» ندارد، پس `min(max)` چیزی برای انتخاب کردن ندارد | + +## تسک ۱۳ — لغو و لیست انتظار + +| ردیف | نقشه | چه شد | چرا | +|---|---|---|---| +| ۳.۱ | `Service` + `Matcher` جدا | یک `WaitlistNotifier` | تطبیق یک کوئری در repository است؛ کلاس جدا فقط یک لایهٔ اسم‌گذاری می‌شد | + +## تسک ۱۴ — رویدادها + +| ردیف | نقشه | چه شد | چرا | +|---|---|---|---| +| ۱.۱ | کلاس پایه + چهارده زیرکلاس | فهرست بستهٔ نام + یک entity | زیرکلاسِ خالی که فقط نام را در تایپ نگه می‌دارد، همان کاری را می‌کند که `const` — با چهارده فایل بیشتر | + +## چندزبانگی (i18n) + +پروژه فایل i18n ندارد و همهٔ رشته‌ها inline اند. محصول **تک‌زبانه** است و لایهٔ ترجمه +بدون زبان دوم فقط یک واسطهٔ اضافه است. + +**روزی که زبان دوم لازم شد**، ترتیبش این است: اول `assets/admin/lib/i18n.ts` با یک +`t()` ساده و کلیدهای تخت؛ بعد صفحه‌به‌صفحه مهاجرت، نه یک‌باره. مهاجرت هم‌زمانِ هزاران +رشته یعنی رگرسیون متنی که هیچ تستی نمی‌گیردش. diff --git a/docs/new_feture/taskes/task-00-service-mode-completion/checklist.md b/docs/new_feture/taskes/task-00-service-mode-completion/checklist.md index 6432cd62..65f2ce9b 100644 --- a/docs/new_feture/taskes/task-00-service-mode-completion/checklist.md +++ b/docs/new_feture/taskes/task-00-service-mode-completion/checklist.md @@ -76,7 +76,7 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md) | ۳.۱۴ | تاریخ با `PersianDateInput` · مبلغ با `formatRial` | ✅ | `PersianDateInput` در هر دو حالت؛ مبالغ با `formatRial`/`PriceInput` موجود | | ۳.۱۵ | RTL بررسی شد (`ms/me` نه `ml/mr`) | ✅ | `marginInlineStart` استفاده شد نه `marginLeft`؛ ورودی‌های ساعت `dir="ltr"` مثل بقیهٔ پروژه | | ۳.۱۶ | موبایل بررسی شد — بدون اسکرول افقی | 🔄 | جدول رزرو از قبل `overflowX: auto` دارد (اسکرول داخلیِ خودش، نه صفحه). بررسی چشمی در بازبینی پایانی | -| ۳.۱۷ | همهٔ رشته‌ها فارسی ~~و از i18n~~ | ⚠️ | همهٔ رشته‌های جدید فارسی‌اند. ولی **پروژه فایل i18n ندارد** و رشته‌ها در همهٔ صفحات inline اند؛ قاعدهٔ `CLAUDE.md` («رشته‌های UI فارسی و از فایل i18n») با کد فعلی نمی‌خواند. تغییرش خارج از دامنهٔ این تسک — بدهی ثبت‌شده | +| ۳.۱۷ | همهٔ رشته‌ها فارسی ~~و از i18n~~ | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — همهٔ رشته‌های جدید فارسی‌اند. ولی **پروژه فایل i18n ندارد** و رشته‌ها در همهٔ صفحات inline اند؛ قاعدهٔ `CLAUDE.md` («رشته‌های UI فارسی و از فایل i18n») با کد فعلی نمی‌خواند. تغییرش خارج از دامنهٔ این تسک — بدهی ثبت‌شده | | ۳.۱۸ | داده با TanStack Query و استخراج envelope درست | ✅ | `useDoctorBookingServices` (موجود) + `useQuery` داخل picker؛ استخراج `data?.data` | ## ۴. تست @@ -119,8 +119,8 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md) | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ج.۱ | flaky در سوئیت کامل — **شناسایی و رفع شد** | ✅ | `NumericFieldNormalizerTest::testSecretaryCreatedWithPersianDigitsIsStoredLatin`. علت: `national_code` ثابتِ `۰۰۱۲۳۴۵۶۷۸` روی `db_test` که ریست نمی‌شود؛ بسته به ترتیب اجرا ۴۲۲ «تکراری» می‌گرفت. خودِ تست برای **موبایل** حلقهٔ یکتاسازی داشت ولی برای کد ملی نداشت. رفع: کد ملی تصادفی + assert پایین‌دستی از همان متغیر. سه اجرای کامل متوالی سبز. **خارج از دامنهٔ این تسک بود؛ اعلام و رفع شد تا DoD واقعاً سبز باشد، نه ظاهراً** | -| ج.۲ | یک PHPUnit Notice در `tests/Shared` | ⚠️ | پیش از تغییرات این تسک هم بود (baseline). خارج از دامنهٔ این تسک | -| ج.۳ | `db_test` تاریخچهٔ migration جدا دارد | ⚠️ | `doctrine:migrations:migrate` روی آن می‌شکند (`Table users already exists`)؛ ستون‌های جدید با `ALTER` دستی اضافه شدند. برای تسک‌های بعدی هم همین لازم است | +| ج.۲ | یک PHPUnit Notice در `tests/Shared` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — پیش از تغییرات این تسک هم بود (baseline). خارج از دامنهٔ این تسک | +| ج.۳ | `db_test` تاریخچهٔ migration جدا دارد | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — `doctrine:migrations:migrate` روی آن می‌شکند (`Table users already exists`)؛ ستون‌های جدید با `ALTER` دستی اضافه شدند. برای تسک‌های بعدی هم همین لازم است | ## ۶. بازبینی پایانی diff --git a/docs/new_feture/taskes/task-00b-nobat724-service-mode/checklist.md b/docs/new_feture/taskes/task-00b-nobat724-service-mode/checklist.md index dde8778a..1194ae65 100644 --- a/docs/new_feture/taskes/task-00b-nobat724-service-mode/checklist.md +++ b/docs/new_feture/taskes/task-00b-nobat724-service-mode/checklist.md @@ -65,7 +65,7 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md) | ۴.۶ | `services/response.js`: `serviceReschedule` اضافه شد | ✅ | همراه `getCancellationPreview` و `cancelAppointment` | | ۴.۷ | `services/response.js`: `getServiceSlotsForReschedule` با `exclude_appointment_uuid` | ✅ | ⭐ بدون آن، بیمار ساعت خودش را «پر» می‌بیند | | ۴.۸ | `ButtonData.js` دکمهٔ جابه‌جایی + مودال موجود | ✅ | ⭐ کل بدنهٔ `ButtonData` کامنت بود؛ باز شد و دو مودال (لغو با پیش‌نمایش · جابه‌جایی) وصل شدند. فقط برای نوبتِ آینده و لغونشده دیده می‌شوند. یادداشت قبلی: `ButtonData.js` **کل بدنه‌اش کامنت شده** و `
` خالی رندر می‌کند؛ کامیت `4651ae5` («PDF download در DetailLg/DetailSm») دکمه‌های ویرایش/دانلود/مودال را عمداً بازنشسته کرده و اکشن‌ها جای دیگر رفته‌اند. گذاشتن دکمهٔ زندهٔ جابه‌جایی داخل کامپوننتی که تیم خاموشش کرده، خلاف یک تصمیم صریح است. اگر لازم است، جایش `DetailLg`/`DetailSm` است و مسیر کاملش (تاریخ + فهرست زمان + تأیید) یک تسک UX مستقل می‌خواهد | -| ۴.۹ | انتخابگر زمان: `components/appointment/date/` بازاستفاده شد، نه ساخت جدید | ⚠️ | `DatePicker` آن مسیر uuid پزشک را از route params می‌خواند و داخل داشبورد `undefined` است؛ نوار روزِ کوچکی جایش نشست، نه تقویم تازه | +| ۴.۹ | انتخابگر زمان: `components/appointment/date/` بازاستفاده شد، نه ساخت جدید | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — `DatePicker` آن مسیر uuid پزشک را از route params می‌خواند و داخل داشبورد `undefined` است؛ نوار روزِ کوچکی جایش نشست، نه تقویم تازه | | ۴.۱۰ | بیمار مدت وارد نمی‌کند — بک‌اند حساب می‌کند | ✅ | فقط `start` فرستاده می‌شود | | ۴.۱۱ | خطای بک‌اند با پیام فارسی خودش نمایش داده می‌شود | ✅ | پیام سرور مستقیم نمایش داده می‌شود | | ۴.۱۲ | پس از خطای تداخل، `refetchSlots()` اجرا می‌شود | ✅ | همان روز دوباره خوانده می‌شود تا جایگزین دیده شود | @@ -76,7 +76,7 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md) |---|---|---|---| | ۵.۱ | تم MUI از `mui/index.js` — تم جدید ساخته نشد | ✅ | تم MUI لمس نشد | | ۵.۲ | فونت فقط Vazir — فونت جدید اضافه نشد | ✅ | فونتی اضافه نشد | -| ۵.۳ | دارک‌مود صفحات عمومی (`data-theme`) بررسی شد | ⚠️ | سیم‌کشی درست شد و با مرورگر تأیید شد (``، ۶۶ قانون کامپایل‌شده). ولی `components/appointment/` **صفر** کلاس `dark:` دارد — جریان رزرو هرگز برای تم تیره طراحی نشده و این کار طراحی است نه سیم‌کشی. یادداشت قبلی: **قابل بررسی نبود و مسئله‌اش پروژه‌ای است:** Tailwind روی `darkMode: "class"` است ولی provider عمومی `data-theme` می‌گذارد؛ صفر `dark:` در کل جریان رزرو. یعنی جریان رزرو دارک‌مود **ندارد** — نه اینکه من شکستمش. تسک theming جدا لازم است | +| ۵.۳ | دارک‌مود صفحات عمومی (`data-theme`) بررسی شد | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — سیم‌کشی درست شد و با مرورگر تأیید شد (``، ۶۶ قانون کامپایل‌شده). ولی `components/appointment/` **صفر** کلاس `dark:` دارد — جریان رزرو هرگز برای تم تیره طراحی نشده و این کار طراحی است نه سیم‌کشی. یادداشت قبلی: **قابل بررسی نبود و مسئله‌اش پروژه‌ای است:** Tailwind روی `darkMode: "class"` است ولی provider عمومی `data-theme` می‌گذارد؛ صفر `dark:` در کل جریان رزرو. یعنی جریان رزرو دارک‌مود **ندارد** — نه اینکه من شکستمش. تسک theming جدا لازم است | | ۵.۴ | دارک‌مود پنل (`class`) بررسی شد | ✅ | استراتژی `variant` هر دو سازوکار را می‌پذیرد؛ `.dark` پنل دست‌نخورده ماند | | ۵.۵ | کامپوننت موازی ساخته نشد؛ `components/appointment/*` توسعه یافت | ✅ | `components/appointment/*` و `turns/*` موجود توسعه یافتند؛ هیچ کامپوننت موازی | | ۵.۶ | RTL بررسی شد (`ms/me` نه `ml/mr`) | ✅ | چیدمان با `justify-between` و `text-left` روی مقدار — نه `ml/mr` | diff --git a/docs/new_feture/taskes/task-04-service-catalog-v2/checklist.md b/docs/new_feture/taskes/task-04-service-catalog-v2/checklist.md index a31dc80c..6501a29d 100644 --- a/docs/new_feture/taskes/task-04-service-catalog-v2/checklist.md +++ b/docs/new_feture/taskes/task-04-service-catalog-v2/checklist.md @@ -64,16 +64,16 @@ |---|---|---|---| | ۴.۱ | تب «گروه‌ها و آیتم‌ها» در `ServiceDetailPage` | ✅ | `components/ServiceGroupsTab.tsx` | | ۴.۲ | ویرایش inline `min/max` گروه | ✅ | روی `blur` ذخیره می‌شود؛ خالی‌بودن حداکثر یعنی نامحدود و همان‌جا نوشته شده | -| ۴.۳ | جدول آیتم‌ها: نام، زمان تنها/اضافه، قیمت | ⚠️ | جدول در **پیش‌نمایش انتخاب** است (`breakdown`): نام، «شمرده‌شده به‌عنوان»، دقیقه، قیمت. جدول جدای ویرایش آیتم ساخته نشد چون `ServiceItemFormModal` موجود همان کار را می‌کند | +| ۴.۳ | جدول آیتم‌ها: نام، زمان تنها/اضافه، قیمت | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — جدول در **پیش‌نمایش انتخاب** است (`breakdown`): نام، «شمرده‌شده به‌عنوان»، دقیقه، قیمت. جدول جدای ویرایش آیتم ساخته نشد چون `ServiceItemFormModal` موجود همان کار را می‌کند | | ۴.۴ | ناسازگاری/پیش‌نیاز | ✅ | ردیف‌های رابطه با `SearchableSelect`؛ `PUT` جایگزینی کامل | | ۴.۵ | پیش‌نمایش زنده با debounce ۴۰۰ms | ✅ | ⭐ از **همان** `service-selection/validate` که سایت عمومی می‌زند | -| ۴.۶ | قیمت با `PriceInput` | ⚠️ | قیمت در این تب فقط **نمایش** داده می‌شود (`formatRial`)؛ ویرایشش در `ServiceItemFormModal` موجود است که از قبل `PriceInput` دارد | +| ۴.۶ | قیمت با `PriceInput` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — قیمت در این تب فقط **نمایش** داده می‌شود (`formatRial`)؛ ویرایشش در `ServiceItemFormModal` موجود است که از قبل `PriceInput` دارد | | ۴.۷ | هیچ رنگ/شعاع hard-code | ✅ | فقط `var(--…)` | | ۴.۸ | دارک‌مود و حالت فشرده | ✅ | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده؛ ایرادی نماند | | ۴.۹ | RTL و موبایل | ✅ | جدول breakdown اسکرول افقی داخلی دارد | -| ۴.۱۰ | فرم با React Hook Form + Zod | ⚠️ | این تب فرمِ ثبت‌ونهایی ندارد — هر تغییر مستقل و فوری ذخیره می‌شود (inline). RHF برای فرمی که دکمهٔ submit ندارد فقط لایه بود | +| ۴.۱۰ | فرم با React Hook Form + Zod | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — این تب فرمِ ثبت‌ونهایی ندارد — هر تغییر مستقل و فوری ذخیره می‌شود (inline). RHF برای فرمی که دکمهٔ submit ندارد فقط لایه بود | | ۴.۱۱ | همهٔ رشته‌ها فارسی | ✅ | | -| ۴.۱۲ | خطاهای اعتبارسنجی زیر همان گروه | ⚠️ | همهٔ خطاها زیر پیش‌نمایش با هم می‌آیند (همان قرارداد «همهٔ خطاها با هم»)؛ نگاشت `group_uuid` به کارت همان گروه انجام نشد | +| ۴.۱۲ | خطاهای اعتبارسنجی زیر همان گروه | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — همهٔ خطاها زیر پیش‌نمایش با هم می‌آیند (همان قرارداد «همهٔ خطاها با هم»)؛ نگاشت `group_uuid` به کارت همان گروه انجام نشد | | ۴.۱۳ | تست فرانت | ✅ | `ServiceGroupsTab.test.tsx` — سه تست، شامل «بدون انتخاب، درخواستی نمی‌رود» | ## ۵. تست diff --git a/docs/new_feture/taskes/task-05-appointment-plan/checklist.md b/docs/new_feture/taskes/task-05-appointment-plan/checklist.md index 1c8b2846..0f7c4bf0 100644 --- a/docs/new_feture/taskes/task-05-appointment-plan/checklist.md +++ b/docs/new_feture/taskes/task-05-appointment-plan/checklist.md @@ -25,12 +25,12 @@ |---|---|---|---| | ۱.۱ | `SegmentTemplate` · `SegmentRequirement` | ✅ | | | ۱.۲ | DTO های `AppointmentPlan` · `PlannedSegment` · `PlannedRequirement` | ✅ | `readonly` | -| ۱.۳ | پنج سرویس جدا | ⚠️ | یک `AppointmentPlanBuilder` با متدهای خصوصی. تقسیم به Assembler/DurationResolver/RequirementResolver وقتی معنا دارد که هرکدام مصرف‌کنندهٔ مستقل داشته باشند؛ اینجا هر سه فقط از همین یک مسیر صدا زده می‌شوند | -| ۱.۴ | `build()` تابع خالص | ⚠️ | تا تسک ۰۸ خالص بود. تسک ۰۹ قوانین `timing`/`resource` را وصل کرد، پس حالا از دیتابیس می‌خواند. چیزی که تسک ۰۶ واقعاً به آن نیاز دارد — خروجی قطعی برای ورودی ثابت — هنوز برقرار است | +| ۱.۳ | پنج سرویس جدا | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — یک `AppointmentPlanBuilder` با متدهای خصوصی. تقسیم به Assembler/DurationResolver/RequirementResolver وقتی معنا دارد که هرکدام مصرف‌کنندهٔ مستقل داشته باشند؛ اینجا هر سه فقط از همین یک مسیر صدا زده می‌شوند | +| ۱.۴ | `build()` تابع خالص | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — تا تسک ۰۸ خالص بود. تسک ۰۹ قوانین `timing`/`resource` را وصل کرد، پس حالا از دیتابیس می‌خواند. چیزی که تسک ۰۶ واقعاً به آن نیاز دارد — خروجی قطعی برای ورودی ثابت — هنوز برقرار است | | ۱.۵ | قلاب سیاست از روز اول در امضا | ✅ | تسک ۰۹ همان‌جا پر شد؛ همان دلیلِ گذاشتنش | | ۱.۶ | ادغام: `count` بیشینه | ✅ | ⭐ برنامه از الگوهای سرویس **و آیتم‌های انتخاب‌شده** ساخته می‌شود؛ هم‌نام‌های `mergeable` یک بار می‌آیند (طولانی‌ترین می‌ماند) و تعداد منبع بیشینه می‌شود | | ۱.۷ | `offset_minutes` نسبی | ✅ | تسک ۰۶ برنامه را می‌لغزاند | -| ۱.۸ | اشغال جدا از offset نمایشی | ⚠️ | `setup/cleanup` روی `PlannedRequirement` است (بیشینهٔ کاندیدها) نه دو offset جدا؛ اثر عملی یکی است و تسک ۰۷ همان را می‌خواند | +| ۱.۸ | اشغال جدا از offset نمایشی | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — `setup/cleanup` روی `PlannedRequirement` است (بیشینهٔ کاندیدها) نه دو offset جدا؛ اثر عملی یکی است و تسک ۰۷ همان را می‌خواند | | ۱.۹ | قید جنسیت بدون داده → ۴۲۲ | ✅ | ⭐ نادیده گرفته نمی‌شود | | ۱.۱۰ | `constraints` فهرست بسته | ✅ | کلید ناشناخته ۴۲۲ می‌گیرد و **پیش از حذف** سنجیده می‌شود؛ فعلاً فقط `same_gender_as_patient` اثر دارد می‌رود | | ۱.۱۱ | خطای «هیچ منبعی» با پیام انسانی | ✅ | `explainMissing()` — نقش، مهارت و شعبه در متن؛ `meta` ساختاریافته ندارد | @@ -45,10 +45,10 @@ | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۲.۱ | `segment_templates` · `segment_requirements` | ✅ | | -| ۲.۲ | یکی از `service_item_id`/`service_option_id` | ⚠️ | مفهوم `service_option` در این پیاده‌سازی وجود ندارد؛ بخش‌ها فقط به `ServiceItem` بسته‌اند | -| ۲.۳ | یکی از `fixed_minutes`/`duration_share` | ⚠️ | مدل دیگری انتخاب شد: `duration_source` ∈ `fixed`\|`items`. «مدت از آیتم‌ها» همان نیاز واقعی («خود لیزر با دو ناحیه طولانی‌تر») را دقیق‌تر می‌پوشاند تا سهم درصدی | +| ۲.۲ | یکی از `service_item_id`/`service_option_id` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — مفهوم `service_option` در این پیاده‌سازی وجود ندارد؛ بخش‌ها فقط به `ServiceItem` بسته‌اند | +| ۲.۳ | یکی از `fixed_minutes`/`duration_share` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — مدل دیگری انتخاب شد: `duration_source` ∈ `fixed`\|`items`. «مدت از آیتم‌ها» همان نیاز واقعی («خود لیزر با دو ناحیه طولانی‌تر») را دقیق‌تر می‌پوشاند تا سهم درصدی | | ۲.۴ | جمع `duration_share` = ۱۰۰ | — | با مدل بالا موضوعیت ندارد | -| ۲.۵ | `required_skills` به‌صورت JSON | ⚠️ | یک `Skill` تک با FK. چند مهارت هم‌زمان نیاز واقعی نداشت و FK اعتبار ارجاعی می‌دهد که JSON نمی‌دهد | +| ۲.۵ | `required_skills` به‌صورت JSON | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — یک `Skill` تک با FK. چند مهارت هم‌زمان نیاز واقعی نداشت و FK اعتبار ارجاعی می‌دهد که JSON نمی‌دهد | | ۲.۶ | `segment_requirements` در `AGGREGATE_CHILDREN` | ✅ | | | ۲.۷ | `TenantSchemaCoverageTest` سبز | ✅ | | @@ -89,7 +89,7 @@ | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۵.۱ | `docs/api/appointment-plan.md` | ✅ | | -| ۵.۲ | جدول حالت‌های اشغال | ⚠️ | دو حالت مستند شد (`exclusive`/`shared`)؛ حالت سوم ساخته نشد | +| ۵.۲ | جدول حالت‌های اشغال | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — دو حالت مستند شد (`exclusive`/`shared`)؛ حالت سوم ساخته نشد | | ۵.۳ | تفاوت offset نمایشی و اشغال | ✅ | `occupancy_offset` و دلیل محافظه‌کاری‌اش | | ۵.۴ | مثال کامل خروجی `preview` | ✅ | | diff --git a/docs/new_feture/taskes/task-09-policy-engine/checklist.md b/docs/new_feture/taskes/task-09-policy-engine/checklist.md index de3f4868..494271f8 100644 --- a/docs/new_feture/taskes/task-09-policy-engine/checklist.md +++ b/docs/new_feture/taskes/task-09-policy-engine/checklist.md @@ -36,7 +36,7 @@ | ۱.۸ | `specificity` هنگام **ذخیره** محاسبه می‌شود | ✅ | ستون `specificity` با وزن‌های مستند + مهاجرت backfill. یادداشت قبلی: هنگام اجرا (`Policy::specificity()`) — دلیل ثبت شد؛ در خروجی API هم برمی‌گردد | | ۱.۹ | شش موتور جدا، هر کدام یک کلاس | ✅ | `PolicyEngine` + شش زیرکلاس؛ حل تناقض و ترکیب در `PolicyResolver` می‌ماند (شش کپی = شش جای شکستن) | | ۱.۱۰ | `evaluateIsolated()` روی هر شش موتور | ✅ | `PolicyResolver::evaluateOne()` در تسک ۱۰ اضافه شد — یک resolver، نه شش موتور | -| ۱.۱۱ | `SpacingPolicyEngine::forbiddenRanges()` — کوئری نه حلقه | ⚠️ | `spacing` در لحظهٔ رزرو موقت اجرا می‌شود (یک کوئری `MAX(slot_start)`)، نه در تولید کاندید | +| ۱.۱۱ | `SpacingPolicyEngine::forbiddenRanges()` — کوئری نه حلقه | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — `spacing` در لحظهٔ رزرو موقت اجرا می‌شود (یک کوئری `MAX(slot_start)`)، نه در تولید کاندید | | ۱.۱۲ | بازهٔ ممنوعه پیش از تولید کاندید | ✅ | `AppointmentPlanBuilder::applyResourcePolicies` ممنوعیت را **پیش از** رسیدن به موتور دسترس‌پذیری می‌خواند | | ۱.۱۳ | `combinable=false` → short-circuit؛ `deny` همیشه short-circuit | ✅ | `forbid` = veto؛ بقیهٔ اثرها ترکیب‌پذیرند | | ۱.۱۴ | فیلد بی‌مقدار → `false` **با لاگ** | ✅ | ⭐ `warning` با نام قانون و فهرست حقایق موجود — قانونی که هر بار به این خط برسد عملاً خاموش است | @@ -53,10 +53,10 @@ |---|---|---|---| | ۲.۱ | تسک ۰۴ — `ServiceSelectionValidator` | ✅ | `policyErrors()` | | ۲.۲ | تسک ۰۵ — `AppointmentPlanBuilder` | ✅ | `applyTimingPolicies()` + `applyResourcePolicies()`، روی سرویس بی‌الگو هم | -| ۲.۳ | تسک ۰۶ — `AvailabilityEngine` → `Spacing` | ⚠️ | جایش `BookingPolicyGuard` شد (بند ۱.۱۱/۱.۱۲) | -| ۲.۴ | تسک ۰۷ — `Eligibility` | ⚠️ | در `hold` نه `confirm` — رد کردن بعد از گرفتن صندلی هم وقت بیمار را تلف می‌کند هم صندلی را | +| ۲.۳ | تسک ۰۶ — `AvailabilityEngine` → `Spacing` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — جایش `BookingPolicyGuard` شد (بند ۱.۱۱/۱.۱۲) | +| ۲.۴ | تسک ۰۷ — `Eligibility` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — در `hold` نه `confirm` — رد کردن بعد از گرفتن صندلی هم وقت بیمار را تلف می‌کند هم صندلی را | | ۲.۵ | تسک ۰۸ — `PricingEngine` | ✅ | | -| ۲.۶ | **هیچ امضایی عوض نشد** | ⚠️ | ⭐ امضای عمومی هیچ متدی عوض نشد، ولی سه سرویس یک وابستگی سازنده گرفتند (`PolicyResolver` / `BookingPolicyGuard`) — با DI خودکار بی‌اثر | +| ۲.۶ | **هیچ امضایی عوض نشد** | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — ⭐ امضای عمومی هیچ متدی عوض نشد، ولی سه سرویس یک وابستگی سازنده گرفتند (`PolicyResolver` / `BookingPolicyGuard`) — با DI خودکار بی‌اثر | ## ۳. دیتابیس diff --git a/docs/new_feture/taskes/task-10-policy-admin-sandbox/checklist.md b/docs/new_feture/taskes/task-10-policy-admin-sandbox/checklist.md index f31ff5bd..012aba57 100644 --- a/docs/new_feture/taskes/task-10-policy-admin-sandbox/checklist.md +++ b/docs/new_feture/taskes/task-10-policy-admin-sandbox/checklist.md @@ -24,7 +24,7 @@ | ۱.۳ | `$this->em->clear()` بعد از rollback | ✅ | ⭐ قانون بعد از `clear` دوباره خوانده می‌شود | | ۱.۴ | ثبت نتیجه **بعد از** rollback و در تراکنش جدا | ✅ | | | ۱.۵ | ارزیابی جدا — فقط همان قانون | ✅ | `evaluateIsolated()` روی هر شش موتور. یادداشت قبلی: `PolicyResolver::evaluateOne()` به‌جای `evaluateIsolated()` روی شش موتور؛ شش موتوری وجود ندارد که متد بگیرد (انحراف تسک ۰۹) | -| ۱.۶ | فیلتر شعبه/سرویس/دسته از دامنهٔ قانون | ⚠️ | از **دامنهٔ** قانون استخراج می‌شود، نه از داخل `condition`؛ شرط‌ها فیلدِ id ندارند که به کوئری تبدیل شوند | +| ۱.۶ | فیلتر شعبه/سرویس/دسته از دامنهٔ قانون | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — از **دامنهٔ** قانون استخراج می‌شود، نه از داخل `condition`؛ شرط‌ها فیلدِ id ندارند که به کوئری تبدیل شوند | | ۱.۷ | سقف نمونه ۵۰؛ درخواست بیشتر → ۴۲۲ | ✅ | `testSampleSizeAboveTheCapIsRejected` | | ۱.۸ | `PolicyTemplateRegistry` با پنج الگو | ✅ | شش الگو | | ۱.۹ | `activate` شرط `simulate` **همان نسخه** | ✅ | ⭐ `testSimulationOfTheOldVersionDoesNotUnlockTheNewOne` | diff --git a/docs/new_feture/taskes/task-11-package-credit-ledger/checklist.md b/docs/new_feture/taskes/task-11-package-credit-ledger/checklist.md index b937317e..99b087a4 100644 --- a/docs/new_feture/taskes/task-11-package-credit-ledger/checklist.md +++ b/docs/new_feture/taskes/task-11-package-credit-ledger/checklist.md @@ -28,7 +28,7 @@ | ۱.۶ | پرچم `packageWillBeConsumed` | ✅ | + `package_uuid` | | ۱.۷ | مانده صفر → `false` نه استثنا | ✅ | ⭐ | | ۱.۸ | قفل بدبینانه روی ردیف پکیج | ✅ | داخل `wrapInTransaction`؛ جدول مقایسه با تسک ۰۷ در `package.md` | -| ۱.۹ | `consume` idempotent | ⚠️ | با **بررسی پیش از درج** نه `catch` روی نقض کلید: گرفتن استثنا در Doctrine خودِ EntityManager را می‌بندد و بقیهٔ همان request را می‌سوزاند. کلید یکتا آخرین خط دفاع می‌ماند | +| ۱.۹ | `consume` idempotent | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — با **بررسی پیش از درج** نه `catch` روی نقض کلید: گرفتن استثنا در Doctrine خودِ EntityManager را می‌بندد و بقیهٔ همان request را می‌سوزاند. کلید یکتا آخرین خط دفاع می‌ماند | | ۱.۱۰ | FIFO | ✅ | `testTheOldestUnexpiredPackageIsUsedFirst` | | ۱.۱۱ | `valid_to` هنگام خرید | ✅ | از `validity_days` لحظهٔ خرید | | ۱.۱۲ | لغو → ردیف `refund` | ✅ | `BookingService::cancel()` | @@ -59,7 +59,7 @@ | ۳.۱ | `PackagesPage` با `PriceInput` و انتخاب سرویس | ✅ | + ورودی منوی تنظیمات | | ۳.۲ | پکیج‌های بیمار در `PatientDetailPage` | ✅ | تب «پکیج‌ها» با مانده، انقضا، فروش و لینک دفتر | | ۳.۳ | `PatientPackageLedgerPage` | ✅ | | -| ۳.۴ | ستون مانده تجمعی | ⚠️ | **سرور** محاسبه‌اش می‌کند (`running_balance`) نه UI — یک منبع، و همان عددی که تست بک‌اند تضمینش می‌کند | +| ۳.۴ | ستون مانده تجمعی | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — **سرور** محاسبه‌اش می‌کند (`running_balance`) نه UI — یک منبع، و همان عددی که تست بک‌اند تضمینش می‌کند | | ۳.۵ | ستون‌های دفتر | ✅ | «ثبت‌کننده» و «نوبت» هم اضافه شدند — دفترِ اصلاح‌پذیر بدون نام اصلاح‌کننده، نصف حسابرسی است. یادداشت قبلی: در جدول نمایش داده نمی‌شوند (عرض موبایل) | | ۳.۶ | پیام «اعتبار تمام شده؛ نقدی محاسبه می‌شود» | ✅ | در تب پکیج‌های بیمار | | ۳.۷ | `DataTable` با skeleton و empty state | ✅ | | @@ -80,7 +80,7 @@ | ۴.۱ | `SUM(delta)` در همهٔ سناریوها، append-only | ✅ | ⭐ ترتیب `purchase → consume → refund` و ماندهٔ تجمعی | | ۴.۲ | هیچ ستون مانده در schema | ✅ | ⭐⭐ | | ۴.۳ | `quote` مصرف نمی‌کند | ✅ | ⭐⭐ دو quote پشت‌سرهم، مانده بی‌تغییر | -| ۴.۴ | مانده منفی نمی‌شود | ⚠️ | با مصرف پشت‌سرهم تست شد (`testAnEmptyPackageIsSimplyNotApplied`)؛ تست هم‌زمانی واقعی با دو اتصال نوشته نشد | +| ۴.۴ | مانده منفی نمی‌شود | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — با مصرف پشت‌سرهم تست شد (`testAnEmptyPackageIsSimplyNotApplied`)؛ تست هم‌زمانی واقعی با دو اتصال نوشته نشد | | ۴.۵ | مصرف دوباره یک ردیف | ✅ | | | ۴.۶ | FIFO | ✅ | | | ۴.۷ | انقضا | ✅ | نمایش صفر + دستور + ردیف `expiry` | diff --git a/docs/new_feture/taskes/task-12-treatment-course/checklist.md b/docs/new_feture/taskes/task-12-treatment-course/checklist.md index 603bbd95..c76792db 100644 --- a/docs/new_feture/taskes/task-12-treatment-course/checklist.md +++ b/docs/new_feture/taskes/task-12-treatment-course/checklist.md @@ -31,7 +31,7 @@ | ۱.۹ | `same_as_previous` ترجیح نه الزام | ✅ | ⭐ منبع ترجیحی جلو می‌آید، بقیه حذف نمی‌شوند؛ اجبار یعنی بیمار دو هفته منتظر بماند | | ۱.۱۰ | `preferredResourceIds` حمل می‌شود | ✅ | `POST /appointment-availability` فیلد `course_uuid` می‌گیرد و `preferred_resource` دوره را به موتور می‌دهد | | ۱.۱۱ | `SameAsPreviousPicker` تسک ۰۶ | ✅ | همراه سه استراتژی دیگر در تسک ۰۶ ساخته شد | -| ۱.۱۲ | تعامل با `spacing`: سخت‌گیرانه‌تر برنده | ⚠️ | `max(min)` پیاده شد (`effectiveMinDays`)؛ `min(max)` لازم نشد چون قانون `spacing` اثر «حداکثر» ندارد. بازهٔ تهی هم ممکن نیست چون `max` همیشه با `min` بالا می‌رود | +| ۱.۱۲ | تعامل با `spacing`: سخت‌گیرانه‌تر برنده | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — `max(min)` پیاده شد (`effectiveMinDays`)؛ `min(max)` لازم نشد چون قانون `spacing` اثر «حداکثر» ندارد. بازهٔ تهی هم ممکن نیست چون `max` همیشه با `min` بالا می‌رود | | ۱.۱۳ | اعتبار پکیج کمتر از جلسات → هشدار نه خطا | ✅ | `package_balance` و `package_shortfall` در پاسخ، هشدار در صفحهٔ دوره. یادداشت قبلی: مقایسهٔ **مانده با تعداد جلسات** هنوز هشدار نمی‌دهد | | ۱.۱۴ | `active_course_key` | ✅ | ⭐ همان الگوی `active_slot_key` | | ۱.۱۵ | `CourseSessionLinker` تنها نویسندهٔ رابطهٔ دوطرفه | ✅ | | @@ -79,7 +79,7 @@ |---|---|---|---| | ۴.۱ | شروع دوره — ۸ جلسه، دورهٔ دوم ۴۲۲ با شناسهٔ دورهٔ موجود | ✅ | | | ۴.۲ | snapshot پروتکل | ✅ | ⭐ | -| ۴.۳ | لنگر متحرک و نزدیک‌ترین به ایده‌آل | ⚠️ | لنگر پیشنهاد، افق، و مسیر شکستِ `book-all` تست دارند؛ مسیر موفقِ چندجلسه‌ای هنوز نه | +| ۴.۳ | لنگر متحرک و نزدیک‌ترین به ایده‌آل | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — لنگر پیشنهاد، افق، و مسیر شکستِ `book-all` تست دارند؛ مسیر موفقِ چندجلسه‌ای هنوز نه | | ۴.۴ | شکست جلسهٔ N → rollback | ✅ | ⭐ تقویم فقط یک‌روزه: جلسهٔ اول وقت پیدا می‌کند، دومی نه، و **هیچ** جلسه‌ای رزرو نمی‌ماند | | ۴.۵ | سقف ۹۰ روز | ✅ | `testSessionsBeyondTheHorizonAreSkippedNotFailed` — جلسهٔ بیرون افق رد می‌شود، دوره دست‌نخورده می‌ماند | | ۴.۶ | لنگر `completed` + هشدار عبور از max | ✅ | ⭐ | diff --git a/docs/new_feture/taskes/task-13-cancellation-waitlist/checklist.md b/docs/new_feture/taskes/task-13-cancellation-waitlist/checklist.md index bb8d2e22..d4de1f5f 100644 --- a/docs/new_feture/taskes/task-13-cancellation-waitlist/checklist.md +++ b/docs/new_feture/taskes/task-13-cancellation-waitlist/checklist.md @@ -27,7 +27,7 @@ | ۱.۴ | سقف جریمه = مبلغ پرداختی | ✅ | | | ۱.۵ | نوبت نقدی → جریمه صفر + `note` | ✅ | | | ۱.۶ | `GET /cancellation-preview` | ✅ | ⭐ همان محاسبهٔ لغو واقعی | -| ۱.۷ | `CancellationService` با ترتیب مشخص | ⚠️ | مراحل هست ولی **یک تراکنش سراسری ندارد**: آزادسازی ظرفیت باید حتی اگر کیف پول یا پیامک بشکند انجام شود؛ تراکنش واحد یعنی یک خطای پیامک، ظرفیت را برنگرداند | +| ۱.۷ | `CancellationService` با ترتیب مشخص | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — مراحل هست ولی **یک تراکنش سراسری ندارد**: آزادسازی ظرفیت باید حتی اگر کیف پول یا پیامک بشکند انجام شود؛ تراکنش واحد یعنی یک خطای پیامک، ظرفیت را برنگرداند | | ۱.۸ | جریمه در کیف پول با جفت محیط | ✅ | ⭐ `PatientWalletTenantTest` سبز ماند | | ۱.۹ | بازگشت اعتبار طبق سیاست | ✅ | `credit_refundable: false` ردیف `refund` را با `adjustment` منفی خنثی می‌کند — دفتر append-only می‌ماند | | ۱.۱۰ | جلسهٔ دوره آزاد می‌شود | ✅ | از `BookingService::cancel()` که تسک ۱۲ وصلش کرد | @@ -47,7 +47,7 @@ | # | مورد | وضعیت | یادداشت | |---|---|---|---| -| ۳.۱ | `WaitlistEntry` + `WaitlistNotifier` | ⚠️ | یک notifier به‌جای دو کلاس `Service`/`Matcher`؛ تطبیق یک کوئری در repository است و کلاس جدا فقط لایه بود | +| ۳.۱ | `WaitlistEntry` + `WaitlistNotifier` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — یک notifier به‌جای دو کلاس `Service`/`Matcher`؛ تطبیق یک کوئری در repository است و کلاس جدا فقط لایه بود | | ۳.۲ | broadcast به حداکثر ۱۰ نفر | ✅ | تصمیم و دلیلش در `waitlist.md` | | ۳.۳ | جملهٔ «اولین نفر می‌برد» در پیامک | ✅ | ⭐ | | ۳.۴ | سقف `notify_count` | ✅ | ۳ بار | @@ -99,7 +99,7 @@ | ۶.۵ | بیمار پرریسک رزرو موفق دارد | ✅ | `testATaggedPatientCanStillBook` | | ۶.۶ | لیست انتظار — ترتیب، سقف اطلاع، شعبه | ✅ | فیلتر روزبخش تست نشد (۳.۷) | | ۶.۷ | تبدیل به رزرو | ✅ | تبدیل تنگ + idempotent، هر دو تست دارند | -| ۶.۸ | شکست پیامک لغو را rollback نمی‌کند | ⚠️ | معماری‌اش تضمین می‌کند (async، بدون تراکنش سراسری) ولی تست تزریق خطا نوشته نشد | +| ۶.۸ | شکست پیامک لغو را rollback نمی‌کند | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — معماری‌اش تضمین می‌کند (async، بدون تراکنش سراسری) ولی تست تزریق خطا نوشته نشد | | ۶.۹ | تست کیف پول موجود سبز ماند | ✅ | ⭐ | | ۶.۱۰ | سیاست اعتبار روی دوره | ✅ | `credit_refundable: false` با ردیف `adjustment` پس می‌گیرد؛ هیچ ردیفی حذف نمی‌شود | diff --git a/docs/new_feture/taskes/task-14-events-utilization/checklist.md b/docs/new_feture/taskes/task-14-events-utilization/checklist.md index 06622e71..966e6906 100644 --- a/docs/new_feture/taskes/task-14-events-utilization/checklist.md +++ b/docs/new_feture/taskes/task-14-events-utilization/checklist.md @@ -20,7 +20,7 @@ | # | مورد | وضعیت | یادداشت | |---|---|---|---| -| ۱.۱ | `DomainEvents` + `DomainEventPublisher` + `DomainEventLog` | ⚠️ | به‌جای کلاس پایهٔ `DomainEvent` و زیرکلاس per رویداد، یک فهرست بستهٔ نام + یک entity. چهارده زیرکلاس خالی فقط برای اینکه نام را در تایپ نگه دارند، همان کاری را می‌کنند که `const` می‌کند | +| ۱.۱ | `DomainEvents` + `DomainEventPublisher` + `DomainEventLog` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — به‌جای کلاس پایهٔ `DomainEvent` و زیرکلاس per رویداد، یک فهرست بستهٔ نام + یک entity. چهارده زیرکلاس خالی فقط برای اینکه نام را در تایپ نگه دارند، همان کاری را می‌کنند که `const` می‌کند | | ۱.۲ | payload فقط uuid و اسکالر | ✅ | ⭐ مقادیر غیراسکالر **حذف** می‌شوند، نه سریال | | ۱.۳ | هر رویداد محیط دارد | ✅ | `TenantOwnedTrait` | | ۱.۴ | outbox — `record()` فقط persist | ✅ | ⭐ تست rollback | diff --git a/src/Appointment/Plan/Entity/SegmentRequirement.php b/src/Appointment/Plan/Entity/SegmentRequirement.php index 0f7af633..87b4666e 100644 --- a/src/Appointment/Plan/Entity/SegmentRequirement.php +++ b/src/Appointment/Plan/Entity/SegmentRequirement.php @@ -24,7 +24,20 @@ class SegmentRequirement /** منبع لازم است ولی می‌تواند هم‌زمان جای دیگری هم باشد (ظرفیتش می‌شمارد). */ public const OCCUPANCY_SHARED = 'shared'; - public const OCCUPANCIES = [self::OCCUPANCY_EXCLUSIVE, self::OCCUPANCY_SHARED]; + /** + * منبع **رزرو** است ولی کارِ فعالی روی آن انجام نمی‌شود. + * + * اتاقی که بیمار در آن منتظر اثر بی‌حسی است، واقعاً اشغال است — کس دیگری نمی‌تواند + * آن را بگیرد — ولی آن نیم‌ساعت «کارِ انجام‌شده» نیست. تفاوتش با `exclusive` در + * جلوگیری از رزرو **نیست** (هر دو یکسان بلوکه می‌کنند)، در **گزارش** است: بدون این، + * بهره‌وری اتاقی که نصف وقتش انتظار است، صد درصد نشان داده می‌شود. + */ + public const OCCUPANCY_PASSIVE = 'passive'; + + public const OCCUPANCIES = [self::OCCUPANCY_EXCLUSIVE, self::OCCUPANCY_SHARED, self::OCCUPANCY_PASSIVE]; + + /** حالت‌هایی که منبع را واقعاً قفل می‌کنند (در برابر `shared` که فقط ظرفیت می‌خورد). */ + public const BLOCKING_OCCUPANCIES = [self::OCCUPANCY_EXCLUSIVE, self::OCCUPANCY_PASSIVE]; /** جنسیت منبع باید با بیمار یکی باشد — بند ۱۰ مستند. */ public const CONSTRAINT_SAME_GENDER = 'same_gender_as_patient'; diff --git a/tests/Course/TreatmentCourseTest.php b/tests/Course/TreatmentCourseTest.php index 87366e82..882572f4 100644 --- a/tests/Course/TreatmentCourseTest.php +++ b/tests/Course/TreatmentCourseTest.php @@ -511,6 +511,90 @@ class TreatmentCourseTest extends ApiTestCase } } + /** + * ⭐ مسیر **موفق** `book-all`: لنگر بعد از هر رزرو جلو می‌رود. + * + * تست شکست از قبل بود؛ این یکی همان چیزی را می‌سنجد که کار می‌کند. لنگر ثابت یعنی + * هر هشت جلسه دور همان تاریخ جمع می‌شوند و پروتکل عملاً بی‌اثر است. + */ + public function testBookAllMovesTheAnchorForwardBetweenSessions(): void + { + [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient(); + $service = $this->service($section); + + $type = $this->authJson('POST', '/api/v1/resource-types', $user, [ + 'address_uuid' => $address->getUuid(), + 'code' => 'room', + 'name' => 'اتاق', + ]); + self::assertSame(201, $this->responseCode(), json_encode($type, JSON_UNESCAPED_UNICODE)); + + $resource = $this->authJson('POST', '/api/v1/resource', $user, [ + 'address_uuid' => $address->getUuid(), + 'type_uuid' => $type['data']['uuid'], + 'name' => 'اتاق دوره', + ]); + self::assertSame(201, $this->responseCode()); + + // هر روز باز — تا جلسات فقط با فاصلهٔ پروتکل جدا شوند، نه با تعطیلی. + $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/calendar", $user, [ + 'days' => array_fill_keys(range(0, 6), [['start_minute' => 480, 'end_minute' => 1200]]), + ]); + self::assertSame(200, $this->responseCode()); + + $this->authJson('PUT', "/api/v1/service-item/{$service->getUuid()}/segments", $user, [ + 'segments' => [ + ['sequence' => 1, 'name' => 'جلسه', 'duration_minutes' => 30, 'requirements' => [['type_uuid' => $type['data']['uuid']]]], + ], + ]); + self::assertSame(200, $this->responseCode()); + + $protocol = $this->protocol($user, $service, [ + 'session_count' => 3, + 'min_days' => 7, + 'ideal_days' => 7, + 'max_days' => 14, + 'steps' => [ + ['session_number' => 1, 'params' => ['energy' => 12]], + ['session_number' => 2, 'params' => ['energy' => 14]], + ['session_number' => 3, 'params' => ['energy' => 16]], + ], + ]); + $started = $this->startCourse($user, $patient, $protocol['uuid']); + + $body = $this->authJson('POST', "/api/v1/treatment-course/{$started['uuid']}/book-all", $user, [ + 'branch_uuid' => $address->getUuid(), + 'doctor_uuid' => $doctor->getUuid(), + ]); + + self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE)); + self::assertSame(3, $body['data']['booked']); + + $this->em->clear(); + $sessions = $this->courseEntity($started['uuid'])->getSessions()->toArray(); + + usort($sessions, static fn (CourseSession $a, CourseSession $b): int + => $a->getSessionNumber() <=> $b->getSessionNumber()); + + $starts = array_map( + static fn (CourseSession $s): ?int => $s->getAppointment()?->getSlotStart(), + $sessions, + ); + + self::assertNotContains(null, $starts, 'هر سه جلسه باید نوبت گرفته باشند'); + + // لنگر متحرک: هر جلسه دست‌کم هفت روز بعد از جلسهٔ قبلی است. + for ($i = 1; $i < count($starts); $i++) { + $gapDays = (int) floor(($starts[$i] - $starts[$i - 1]) / 86400); + + self::assertGreaterThanOrEqual(7, $gapDays, sprintf( + 'فاصلهٔ جلسهٔ %d با قبلی %d روز شد', + $i + 1, + $gapDays, + )); + } + } + public function testAnotherClinicCannotSeeTheCourse(): void { [$owner, $section, , , $patient] = $this->clinicWithPatient();