feat(pricing): date-ranged price lists and immutable appointment invoices

Section 12 and the fifth closing rule: changing a price never changes an
already-booked appointment.

The pricing chain already existed and worked. Two things were missing. Tariff only
carries a year, so a rate change starting in Mehr could not be expressed — PriceList
now takes an explicit date range and Tariff remains the layer beneath it. And an
appointment stored a single number, so after a price change or a discount nobody
could say what those 2,400,000 rials were made of.

Price resolution walks four layers per service and takes the first hit: branch
override, then the covering price list, then the yearly tariff, then the service's own
price. The last one is the guarantee that a date no list covers still returns a price
rather than zero or an exception. breakdown.sources reports which layer answered, so a
surprising number can be traced instead of guessed at.

Two calculation decisions worth stating. Tax is computed on the patient's share, not
the gross — a patient does not pay tax on the portion the insurer covers. And a
discount larger than the amount floors the total at zero rather than going negative,
because a negative balance would mean the clinic owes the patient money, which nothing
downstream is built to mean.

A branch-specific list deliberately does not count as overlapping a general one; it
takes precedence instead. Treating them as a conflict would have made per-branch
exceptions impossible to express. Lists have no effect until activated, so drafting
next quarter's prices cannot disturb today's.

PriceSnapshot has no setters and a unique key on appointment_id: a snapshot that can
be edited is not a snapshot, and two invoices for one appointment would be two truths.
Corrections are a new row plus voiding the old one. Invoices are written during
confirm with the prices of that moment — computing later would let a rate change
between booking and invoicing produce a different number, which is exactly what rule
five forbids.

12 tests. The one that matters is
testBookedAppointmentKeepsItsOriginalInvoiceAfterAPriceChange: book, double the
service price, watch quote return the new number while the appointment's invoice
returns the old one. Without it rule five is only a claim.

1220 tests / 3551 assertions. phpstan back at its 14-error baseline. Frozen slot
contract green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-31 09:42:21 +03:30
co-authored by Claude Opus 5
parent cd12fabe14
commit 34b07421bd
17 changed files with 1765 additions and 66 deletions
+1
View File
@@ -88,6 +88,7 @@ Only **digits** are translated — no characters are stripped, so `IR` in a sheb
| [appointment-plan.md](appointment-plan.md) | Appointment segments and plan preview | 3 |
| [appointment-availability.md](appointment-availability.md) | Multi-resource availability search | 2 |
| [appointment-booking.md](appointment-booking.md) | Holds, confirmation and multi-resource occupancy | 4 |
| [pricing.md](pricing.md) | Date-ranged price lists and appointment invoices | 8 |
| [appointment.md](appointment.md) | Appointments & slot booking | 6 |
| [appointment-settings.md](appointment-settings.md) | Weekly schedule, date overrides, holidays | 14 |
| [payment.md](payment.md) | Payments (Mellat / Sep) | 5 |
+137
View File
@@ -0,0 +1,137 @@
# Pricing API — لیست قیمت بازه‌دار و فاکتور تفکیک‌شده
> **Base:** `/api/v1` · **Auth:** JWT
> مکمل [clinic-services.md](clinic-services.md) و [appointment-booking.md](appointment-booking.md).
---
## دو شکافی که پر شد
زنجیرهٔ قیمت از قبل وجود داشت و کار می‌کرد
(`ServiceItem → Tariff → بیمه → DiscountRule → Invoice → Payment`). دو چیز کم بود:
۱. **`Tariff` فقط سال دارد.** تغییر تعرفه از اول مهر قابل بیان نبود. حالا `PriceList`
بازهٔ دقیق می‌گیرد و `Tariff` لایهٔ پشتیبان می‌ماند.
۲. **روی نوبت فقط یک عدد بود.** بعد از تغییر قیمت یا تخفیف نمی‌شد گفت آن ۲٬۴۰۰٬۰۰۰
ریال از چه تشکیل شده بود. حالا `PriceSnapshot` فاکتور تفکیک‌شدهٔ لحظهٔ ثبت را
نگه می‌دارد.
## زنجیرهٔ قیمت‌گذاری
```
قیمت پایه → + آیتم‌ها → − تخفیف → − بیمهٔ پایه → − تکمیلی → + مالیات → بیعانه
```
برای **هر** سرویس، اولین منبعی که پیدا شود برنده است:
| اولویت | منبع | از کجا |
|---|---|---|
| ۱ | override شعبه | تسک ۰۴ |
| ۲ | لیست قیمتِ حاکم بر آن تاریخ | همین تسک |
| ۳ | `Tariff` سال | لایهٔ موجود |
| ۴ | `ServiceItem.price_rials` | همیشه هست |
مرحلهٔ چهارم ضامن است که **هرگز صفر یا خطا** برنگردد — تاریخی که هیچ لیستی نمی‌پوشاند
باید قیمت بدهد. `breakdown.sources` می‌گوید هر قیمت از کدام لایه آمده.
### دو تصمیم محاسباتی
**مالیات روی سهم بیمار حساب می‌شود، نه روی کل.** بیمار مالیاتِ سهمی که بیمه می‌دهد را
نمی‌پردازد.
**تخفیف بیشتر از مبلغ، مبلغ را صفر می‌کند نه منفی.** بدهی منفی یعنی کلینیک به بیمار
پول بدهکار شود، که هیچ‌جای این جریان معنا ندارد.
`max_total_discount_percent` سقف جمع تخفیف‌هاست: چند تخفیفِ جداگانه که هرکدام منطقی‌اند،
با هم می‌توانند مبلغ را بی‌معنا کنند.
---
## `POST /api/v1/pricing/quote`
```json
{
"service_uuid": "…",
"branch_uuid": "…",
"item_uuids": ["…"],
"at": 1785562200,
"policy": {
"discount_percent": 10,
"max_total_discount_percent": 25,
"insurance_base_percent": 20,
"insurance_supplementary_percent": 50,
"tax_percent": 10,
"deposit_percent": 30
}
}
```
`at` اختیاری است (پیش‌فرض الان) و تعیین می‌کند کدام لیست قیمت حاکم است.
**۲۰۰:** همان شکلی که `price_snapshot` دارد — عمداً یکی، تا «قیمتی که نشان دادیم» و
«قیمتی که ثبت کردیم» نتوانند واگرا شوند.
```json
{
"base_rials": 10000000, "items_rials": 2000000, "discount_rials": 1200000,
"insurance_base_rials": 2160000, "insurance_supplementary_rials": 4320000,
"tax_rials": 432000, "final_rials": 4752000, "deposit_rials": 1425600,
"breakdown": { "discounts": [ ], "sources": { "<service-uuid>": "price_list" } }
}
```
---
## لیست قیمت
| متد | مسیر |
|---|---|
| GET/POST | `/api/v1/price-lists` |
| GET/PATCH/DELETE | `/api/v1/price-list/{uuid}` |
| PUT | `/api/v1/price-list/{uuid}/items` |
| POST | `/api/v1/price-list/{uuid}/activate` |
`address_uuid` تهی‌پذیر است: `null` یعنی «همهٔ شعبه‌های این محیط». لیستِ مخصوصِ یک شعبه
بر لیست عمومی **مقدم** است و با آن **تداخل حساب نمی‌شود** — وگرنه تعریف استثنا برای یک
شعبه ناممکن می‌شد.
**لیست تا فعال نشده هیچ اثری ندارد.** ساختن پیش‌نویس نباید قیمت امروز را عوض کند.
`activate` بازهٔ هم‌پوشان با لیست فعالِ **هم‌دامنه** را `422` می‌کند: یک تاریخ نباید دو
قیمت داشته باشد.
---
## فاکتور نوبت
`GET /api/v1/appointment/{uuid}/price-snapshot`
فاکتور هنگام `POST /appointment-confirm` و با قیمت‌های **همان لحظه** ثبت می‌شود. اگر
بعداً محاسبه می‌شد، تغییر تعرفه بین ثبت و صدور فاکتور عدد دیگری می‌داد.
> **قانون پنجم مستند:** «تغییر قیمت هرگز نوبت‌های ثبت‌شده را عوض نمی‌کند.»
> `PriceSnapshot` هیچ setter ای ندارد و کلید یکتای `appointment_id` دو فاکتور برای یک
> نوبت را در سطح دیتابیس غیرممکن می‌کند. اصلاح قیمت با ردیف تازه و ابطال قبلی انجام
> می‌شود، نه با بازنویسی.
نوبتِ بدون سرویس (ویزیت سادهٔ حالت اسلاتی) هم فاکتور می‌گیرد، با همان
`visit_price_rials` موجود — خالی گذاشتنش یعنی گزارش مالی یک ردیف کم دارد.
---
## طبقه‌بندی محیط
| جدول | وضعیت |
|---|---|
| `price_lists` · `price_snapshots` | جفت محیط |
| `price_list_items` | `AGGREGATE_CHILDREN` — ریشه `PriceList` |
## تست‌ها
```bash
ddev exec php bin/phpunit tests/Pricing # ۱۲ تست
```
مهم‌ترینش `testBookedAppointmentKeepsItsOriginalInvoiceAfterAPriceChange` است: نوبت ثبت
می‌شود، قیمت سرویس دو برابر می‌شود، `quote` عدد جدید می‌دهد و فاکتور نوبت **همان عدد
قبلی** را. بدون آن، قانون پنجم فقط یک ادعاست.
@@ -1,6 +1,6 @@
# چک‌لیست — تسک ۰۸ (لیست قیمت بازه‌دار و 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)
@@ -11,98 +11,98 @@
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | | |
| ۰.۲ | `Tariff` دست‌نخورده — سطر ۴ زنجیرهٔ `PriceResolver` | | |
| ۰.۳ | `AppointmentInsuranceService` و `TenantServiceCoverage` بازنویسی **نشدند** | | قاعدهٔ «اول بگرد» |
| ۰.۴ | `DiscountRule`/`DiscountEngine` دست‌نخورده | | |
| ۰.۵ | `Invoice`/`InvoiceItem` دست‌نخورده و حذف نشدند | | کار متفاوتی می‌کنند |
| ۰.۶ | ستون‌های موجود نوبت استفاده شدند، ستون جدید مالی روی `appointments` اضافه نشد | | |
| ۰.۱ | `--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 | | |
| ۱.۱ | `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` سبز | | |
| ۲.۱ | پنج جدول | | |
| ۲.۲ | `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` · `PriceListFormPage` | ⏳ | |
| ۳.۲ | وضعیت شمسی: پیش‌نویس/فعال/منقضی با `StatusBadge` | ⏳ | |
| ۳.۳ | بازهٔ تاریخ با `PersianDatePicker` | ⏳ | |
| ۳.۴ | قیمت‌ها با `PriceInput` | ⏳ | |
| ۳.۵ | شعبه با `SearchableSelect` | ⏳ | |
| ۳.۶ | **«کپی از لیست قیمت قبلی»** | ⏳ | ⭐ با ۲۰۰ سرویس بدون آن لیست جدید ساخته نمی‌شود |
| ۳.۷ | کارت «فاکتور» در `AppointmentDetailPage` با ردیف‌های snapshot | ⏳ | |
| ۳.۸ | متن «قیمت بر اساس تاریخ نوبت محاسبه شده است» | ⏳ | |
| ۳.۹ | هیچ رنگ/شعاع hard-code | ⏳ | |
| ۳.۱۰ | دارک‌مود و حالت فشرده | ⏳ | |
| ۳.۱۱ | RTL و موبایل | ⏳ | |
| ۳.۱۲ | مبالغ با `formatRial` · تاریخ‌ها با `formatDate` | ⏳ | |
| ۳.۱۳ | وضعیت لیست در URL با `useUrlState` | ⏳ | |
| ۳.۱۴ | همهٔ رشته‌ها فارسی | ⏳ | |
| ۳.۱ | `PriceListsPage` · `PriceListFormPage` | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۲ | وضعیت شمسی: پیش‌نویس/فعال/منقضی با `StatusBadge` | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۳ | بازهٔ تاریخ با `PersianDatePicker` | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۴ | قیمت‌ها با `PriceInput` | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۵ | شعبه با `SearchableSelect` | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۶ | **«کپی از لیست قیمت قبلی»** | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۷ | کارت «فاکتور» در `AppointmentDetailPage` با ردیف‌های snapshot | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۸ | متن «قیمت بر اساس تاریخ نوبت محاسبه شده است» | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۹ | هیچ رنگ/شعاع hard-code | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۱۰ | دارک‌مود و حالت فشرده | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۱۱ | RTL و موبایل | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۱۲ | مبالغ با `formatRial` · تاریخ‌ها با `formatDate` | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۱۳ | وضعیت لیست در URL با `useUrlState` | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۳.۱۴ | همهٔ رشته‌ها فارسی | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
## ۴. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `PriceResolverTest` — ترتیب پنج‌گانه + fallback | ⏳ | |
| ۴.۲ | `PricingEngineTest` — تخفیف پشت‌سرهم، سقف، منفی → صفر | ⏳ | |
| ۴.۳ | **invariant**: جمع ردیف‌ها = مبلغ نهایی، در همهٔ سناریوها | ⏳ | |
| ۴.۴ | `PriceSnapshotImmutabilityTest` — قانون پنجم | ⏳ | ⭐ تغییر قیمت و حذف قانون |
| ۴.۵ | `PriceListActivationTest` — تداخل هم‌سطح ۴۲۲، شعبه/محیط بی‌تداخل | ⏳ | |
| ۴.۶ | `DepositCalculatorTest` — درصدی با min/max، اولویت سرویس | ⏳ | |
| ۴.۷ | `QuoteTenantTest` — سرویس محیط دیگر ۴۰۴ | ⏳ | |
| ۴.۸ | نوبت بدون سرویس (حالت `slot`) → snapshot با `visit_price_rials` | ⏳ | |
| ۴.۱ | `PriceResolverTest` — ترتیب پنج‌گانه + fallback | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۴.۲ | `PricingEngineTest` — تخفیف پشت‌سرهم، سقف، منفی → صفر | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۴.۳ | **invariant**: جمع ردیف‌ها = مبلغ نهایی، در همهٔ سناریوها | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۴.۴ | `PriceSnapshotImmutabilityTest` — قانون پنجم | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۴.۵ | `PriceListActivationTest` — تداخل هم‌سطح ۴۲۲، شعبه/محیط بی‌تداخل | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۴.۶ | `DepositCalculatorTest` — درصدی با min/max، اولویت سرویس | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۴.۷ | `QuoteTenantTest` — سرویس محیط دیگر ۴۰۴ | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
| ۴.۸ | نوبت بدون سرویس (حالت `slot`) → snapshot با `visit_price_rials` | ⏳ | UI این تسک ساخته نشد — اندپوینت‌ها کامل و از API مصرف‌شدنی‌اند. مقصد: پاس UI مالی |
## ۵. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/pricing.md` | | |
| ۵.۲ | تصمیم «تاریخ رزرو، نه تاریخ ثبت» صریح | | |
| ۵.۳ | `docs/architecture/insurance-billing-system.md` جدول `PriceSnapshot` vs `Invoice` | | ⭐ وگرنه یکی حذف می‌شود |
| ۵.۱ | `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 .` | | |
| ۶.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | | |
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | | |
| ۶.۲ | `bin/phpunit` کامل سبز | | |
| ۶.۳ | `--group=slot-mode-frozen` سبز | | |
| ۶.۴ | `phpstan` بدون خطای جدید | | |
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | | |
| ۶.۶ | تست‌های tenant سبز | | |
| ۶.۷ | `docs/api/*` به‌روز | | |
| ۶.۸ | چک‌لیست UI کامل | | |
| ۶.۹ | ⚠️ مبلغ نمایشی رزرو ممکن است عوض شود → دو کلاینت دستی بررسی شدند | | |
| ۶.۱۰ | commit، سپس `graphify update .` | | |
| ۶.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | | |