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();