Files
clinicpro/docs/new_feture/taskes/task-08-pricing-snapshot/checklist.md
T
hamedandClaude Opus 5 34b07421bd 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>
2026-07-31 09:42:21 +03:30

9.5 KiB
Raw Blame History

چک‌لیست — تسک ۰۸ (لیست قیمت بازه‌دار و 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 .
۶.۱۱ موارد به‌تعویق با دلیل و تسک مقصد