Files
hamedandClaude Opus 5 ca2f9b8652 feat(admin): build the last two screens, and pin spacing with a test
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>
2026-08-01 15:32:58 +03:30

8.3 KiB
Raw Permalink 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 لیست + مودال ویرایش با ردیف‌های قیمت
۳.۲ وضعیت شمسی: پیش‌نویس/فعال/منقضی سه حالت با معنای عملیاتی متفاوت؛ فعال‌سازی عمل جداست، نه تیک داخل فرم
۳.۳ بازهٔ تاریخ با 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 .
۶.۱۱ موارد به‌تعویق با دلیل و تسک مقصد