The cancellation policy page showed only the tenant policy, so nothing said which services opt out of it. Service policies do not blend with the tenant one — a service that has its own follows it completely — and without the table an operator cannot tell why one service's penalty differs. It lists them with a link to each service. The waitlist had the matches endpoint and no way to reach it. The list answers "who is waiting"; the question asked when capacity frees up is "who is waiting for this slot", so the page now takes a service and a date and answers that. The note says plainly that cancelling notifies them anyway — this is for looking before deciding, not a second notification path. Spacing is enforced at hold time rather than during candidate generation, which costs one slot being shown and then refused, and saves a patient-history query per candidate. That trade had no test; now a booking five days after the last one is refused and one thirty days later goes through. Checklists across all sixteen tasks are final: no pending rows, and the warnings that remain are recorded decisions — one resolver instead of six engines, a closed list instead of a registry, sample size three instead of ten — each with the reason it was taken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.3 KiB
8.3 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 |
✅ | لیست + مودال ویرایش با ردیفهای قیمت |
| ۳.۲ | وضعیت شمسی: پیشنویس/فعال/منقضی | ✅ | سه حالت با معنای عملیاتی متفاوت؛ فعالسازی عمل جداست، نه تیک داخل فرم |
| ۳.۳ | بازهٔ تاریخ با 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 . |
✅ | |
| ۶.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ |