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>
9.5 KiB
9.5 KiB
چکلیست — تسک ۰۸ (لیست قیمت بازهدار و snapshot فاکتور)
وضعیت کلی: ✅ بکاند و مستندات تکمیل (UI ⏳) · آخرین بازبینی: —
قواعد: _shared/definition-of-done.md · red-lines.md · 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 · 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 |
⏳ | 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 |
✅ | ⭐ وگرنه یکی حذف میشود |
۶. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ✅ | |
| ۶.۲ | bin/phpunit کامل سبز |
✅ | |
| ۶.۳ | --group=slot-mode-frozen سبز |
✅ | |
| ۶.۴ | phpstan بدون خطای جدید |
✅ | |
| ۶.۵ | npx tsc --noEmit و yarn test سبز |
✅ | |
| ۶.۶ | تستهای tenant سبز | ✅ | |
| ۶.۷ | docs/api/* بهروز |
✅ | |
| ۶.۸ | چکلیست UI کامل | ✅ | |
| ۶.۹ | ⚠️ مبلغ نمایشی رزرو ممکن است عوض شود → دو کلاینت دستی بررسی شدند | ✅ | |
| ۶.۱۰ | commit، سپس graphify update . |
✅ | |
| ۶.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ |