# چک‌لیست — تسک ۰۸ (لیست قیمت بازه‌دار و snapshot فاکتور)

**وضعیت کلی:** ✅ تمام‌شده — بک‌اند، مستندات و UI · **آخرین بازبینی:** —

قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)

---

## ۰. خط سرخ

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ✅ | |
| ۰.۲ | `Tariff` دست‌نخورده — سطر ۴ زنجیرهٔ `PriceResolver` | ✅ | |
| ۰.۳ | `AppointmentInsuranceService` و `TenantServiceCoverage` بازنویسی **نشدند** | ✅ | قاعدهٔ «اول بگرد» |
| ۰.۴ | `DiscountRule`/`DiscountEngine` دست‌نخورده | ✅ | |
| ۰.۵ | `Invoice`/`InvoiceItem` دست‌نخورده و حذف نشدند | ✅ | کار متفاوتی می‌کنند |
| ۰.۶ | ستون‌های موجود نوبت استفاده شدند، ستون جدید مالی روی `appointments` اضافه نشد | ✅ | |

## ۱. بک‌اند

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `PriceList` · `PriceListItem` · `PriceSnapshot` · `PriceSnapshotLine` · `DepositPolicy` | ✅ | |
| ۱.۲ | `PricingEngine` — هفت مرحله، هر کدام سرویس مستقل | ✅ | |
| ۱.۳ | مراحل ۳ و ۴ از روز اول در زنجیره، حتی no-op | ✅ | تسک ۰۹ و ۱۱ |
| ۱.۴ | `PriceResolver` — ترتیب پنج‌گانه، هرگز صفر یا خطا | ✅ | |
| ۱.۵ | تاریخ مبنا = `slot_start` (تاریخ رزرو)، نه `time()` | ✅ | ⭐ دو تفسیر دارد |
| ۱.۶ | همهٔ محاسبات با `intdiv`، هیچ float در مسیر پول | ✅ | |
| ۱.۷ | تخفیف **پشت‌سرهم**، نه جمع درصدها | ✅ | ۴۰ سپس ۱۰ = ۴۶ |
| ۱.۸ | سقف جمع تخفیف اعمال شد | ✅ | |
| ۱.۹ | `final = max(0, …)` + ردیف `adjustment` هنگام فعال شدن سقف | ✅ | |
| ۱.۱۰ | `appliedPolicyIds` از روز اول ثبت می‌شود | ✅ | |
| ۱.۱۱ | `PriceSnapshotLine.label` و `source_id` بدون FK (کپی متنی) | ✅ | قانون پنجم |
| ۱.۱۲ | `DepositCalculator` روی ستون‌های موجود نوبت می‌نویسد | ✅ | |
| ۱.۱۳ | `activate` تداخل بازه را می‌سنجد؛ لیست شعبه با محیط تداخل ندارد | ✅ | |
| ۱.۱۴ | هفت endpoint | ✅ | |
| ۱.۱۵ | قلاب مرحلهٔ ۶ `BookingService::confirm` وصل شد | ✅ | |
| ۱.۱۶ | `TenantOwnershipChecker` روی هر uuid از request | ✅ | |

## ۲. دیتابیس

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | پنج جدول | ✅ | |
| ۲.۲ | `final_rials` و `amount_rials` از نوع **BIGINT** | ✅ | ⭐ پکیج بزرگ از سقف INT عبور می‌کند |
| ۲.۳ | بقیهٔ `price_rials` ها `INT` ماندند | ✅ | قیمت واحد عبور نمی‌کند |
| ۲.۴ | `UNIQUE(appointment_id)` روی snapshot | ✅ | |
| ۲.۵ | `price_list_items` و `price_snapshot_lines` در `AGGREGATE_CHILDREN` | ✅ | |
| ۲.۶ | `app:pricing:backfill-snapshots --force` — idempotent | ✅ | نوبت‌های موجود فاکتور خالی نداشته باشند |
| ۲.۷ | `TenantSchemaCoverageTest` سبز | ✅ | |

## ۳. UI

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `PriceListsPage` | ✅ | لیست + مودال ویرایش با ردیف‌های قیمت |
| ۳.۲ | وضعیت شمسی: پیش‌نویس/فعال/منقضی | ✅ | سه حالت با معنای عملیاتی متفاوت؛ فعال‌سازی عمل جداست، نه تیک داخل فرم |
| ۳.۳ | بازهٔ تاریخ با `PersianDateInput` | ✅ | تبدیل ISO↔Unix در همان صفحه |
| ۳.۴ | قیمت‌ها با `PriceInput` | ✅ | |
| ۳.۵ | شعبه با `SearchableSelect` | ✅ | «همهٔ شعبه‌ها» گزینهٔ صریح است، نه خالی‌گذاشتن |
| ۳.۶ | «کپی از لیست قیمت قبلی» | ✅ | ⭐ بازه از پایان لیست قبلی شروع می‌شود |
| ۳.۷ | کارت فاکتور در `AppointmentDetailPage` | ✅ | `AppointmentInvoiceCard` — ردیف‌های صفر پنهان می‌شوند |
| ۳.۸ | متن «قیمت بر اساس تاریخ نوبت محاسبه شده» | ✅ | ⭐ وگرنه کاربری که دیروز تعرفه را عوض کرده فکر می‌کند سیستم خراب است |
| ۳.۹ | هیچ رنگ/شعاع hard-code | ✅ | |
| ۳.۱۰ | دارک‌مود و حالت فشرده | ✅ | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده؛ ایرادی نماند |
| ۳.۱۱ | RTL و موبایل | ✅ | جدول لیست‌ها اسکرول افقی داخلی دارد |
| ۳.۱۲ | مبالغ با `formatRial` · تاریخ با `formatDate` | ✅ | |
| ۳.۱۳ | وضعیت لیست در URL | ✅ | `useUrlState` |
| ۳.۱۴ | همهٔ رشته‌ها فارسی | ✅ | |
| ۳.۱۵ | تست فرانت کارت فاکتور | ✅ | چهار تست، شامل «نبودِ فاکتور خطا نیست» |

## ۴. تست

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | ترتیب لایه‌های قیمت + fallback | ✅ | `testFullChainAppliesInOrder` · `testFallsBackToTheServicePrice` · `testBranchOverrideBeatsThePriceList` |
| ۴.۲ | تخفیف پشت‌سرهم، سقف، منفی → صفر | ✅ | `testDiscountLargerThanTheAmountFloorsAtZero` · `testTotalDiscountCapIsApplied` |
| ۴.۳ | invariant جمع ردیف‌ها = مبلغ نهایی | ✅ | ⭐ `testTheRowsAlwaysAddUpToTheFinalAmount` — خودِ رابطه سنجیده می‌شود نه عددها؛ ردیفِ تازه‌ای که در جمع نیاید همین را قرمز می‌کند. یادداشت قبلی: invariant به‌صورت property-based روی سناریوهای تصادفی نوشته نشد |
| ۴.۴ | تغییرناپذیری فاکتور (قانون پنجم) | ✅ | ⭐ `testBookedAppointmentKeepsItsOriginalInvoiceAfterAPriceChange` |
| ۴.۵ | فعال‌سازی و تداخل بازه | ✅ | `testOverlappingActiveListsAreRejected` · `testBranchListWinsOverTheGeneralList` · `testDraftListHasNoEffectUntilActivated` |
| ۴.۶ | محاسبهٔ بیعانه | ✅ | مبلغی بر درصدی مقدم است و از مبلغ نهایی بیشتر نمی‌شود — هر دو تست دارند. یادداشت قبلی: تست اختصاصی با min/max ندارد |
| ۴.۷ | سرویس محیط دیگر ۴۰۴ | ✅ | `testForeignServiceIsNotFound` |
| ۴.۸ | نوبت بدون سرویس → فاکتور با `visit_price_rials` | ✅ | ثبت بدون `service_uuid` تست شد: فاکتور ساخته می‌شود و `items_rials` صفر است |
| ۴.۹ | قیمت منفی رد می‌شود | ✅ | `testNegativePriceIsRejected` |
| ۴.۱۰ | تست فرانت کارت فاکتور | ✅ | چهار تست |

**اجرا:** `ddev exec php bin/phpunit tests/Pricing` → ۱۲ تست.

## ۵. مستندات

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/pricing.md` | ✅ | |
| ۵.۲ | تصمیم «تاریخ رزرو، نه تاریخ ثبت» صریح | ✅ | |
| ۵.۳ | `docs/architecture/insurance-billing-system.md` جدول `PriceSnapshot` vs `Invoice` | ✅ | ⭐ وگرنه یکی حذف می‌شود |

## ۶. بازبینی پایانی

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ✅ | |
| ۶.۲ | `bin/phpunit` کامل سبز | ✅ | |
| ۶.۳ | `--group=slot-mode-frozen` سبز | ✅ | |
| ۶.۴ | `phpstan` بدون خطای جدید | ✅ | |
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ✅ | |
| ۶.۶ | تست‌های tenant سبز | ✅ | |
| ۶.۷ | `docs/api/*` به‌روز | ✅ | |
| ۶.۸ | چک‌لیست UI کامل | ✅ | |
| ۶.۹ | ⚠️ مبلغ نمایشی رزرو ممکن است عوض شود → دو کلاینت دستی بررسی شدند | ✅ | |
| ۶.۱۰ | commit، سپس `graphify update .` | ✅ | |
| ۶.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ✅ | |
