The last structural gap from task 05 was the third occupancy mode. It is passive: the resource is genuinely held — nobody else can take that room while the patient waits for the anaesthetic — but the time is not work done. It blocks exactly like exclusive; the difference is in the report, where without it a room that spends half its day waiting reads as fully utilised. The mode is validated, offered in the segment editor and carried through to the plan. Everything else that was still marked as a deviation is now recorded in docs/architecture/deviations.md, one row each, in the form "what the plan said / what was built / why". That includes the ones I would defend (five plan services collapsed into one builder that only build() calls; a Skill foreign key instead of a JSON array, because a deleted skill in JSON fails silently) and the ones that are simply facts about the product (service_option does not exist here, so a column for it would sit empty until someone read it as a bug). The i18n section says plainly that the product is single-language and describes the order to migrate in if that changes — a translation layer with one language is an indirection, not an abstraction. All sixteen checklists now read zero pending and zero unresolved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.9 KiB
8.9 KiB
چکلیست — تسک ۱۱ (پکیج و دفتر اعتبار جلسات)
وضعیت کلی: ✅ تمامشده با انحرافهای ثبتشده · آخرین بازبینی: ۱۴۰۵/۰۵/۰۹
قواعد: _shared/definition-of-done.md · red-lines.md · ui-conventions.md
۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | --group=slot-mode-frozen سبز |
✅ | |
| ۰.۲ | هیچ ستون remaining/used_count/balance در هیچ جدولی |
✅ | ⭐⭐ testNoStoredBalanceColumnExists روی schema واقعی |
| ۰.۳ | دفتر append-only | ✅ | هیچ remove/setter روی SessionCreditLedger؛ تصحیح = ردیف تازه |
| ۰.۴ | WalletTransaction دستنخورده |
✅ | تفاوتش در tenancy.md نوشته شد |
۱. بکاند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | چهار entity | ✅ | |
| ۱.۲ | CreditLedgerService تنها نویسندهٔ دفتر |
✅ | فروش، مصرف، بازگشت و اصلاح همه از همین عبور میکنند |
| ۱.۳ | balance() = SUM(delta) |
✅ | ⭐ |
| ۱.۴ | پنج kind |
✅ | سازنده kind ناشناخته و delta صفر را رد میکند |
| ۱.۵ | quote هرگز مصرف نمیکند |
✅ | ⭐⭐ testQuoteAnnouncesThePackageWithoutConsumingIt دو بار quote میزند و مانده را میسنجد |
| ۱.۶ | پرچم packageWillBeConsumed |
✅ | + package_uuid |
| ۱.۷ | مانده صفر → false نه استثنا |
✅ | ⭐ |
| ۱.۸ | قفل بدبینانه روی ردیف پکیج | ✅ | داخل wrapInTransaction؛ جدول مقایسه با تسک ۰۷ در package.md |
| ۱.۹ | consume idempotent |
✅ | تصمیم ثبتشده در deviations.md — با بررسی پیش از درج نه catch روی نقض کلید: گرفتن استثنا در Doctrine خودِ EntityManager را میبندد و بقیهٔ همان request را میسوزاند. کلید یکتا آخرین خط دفاع میماند |
| ۱.۱۰ | FIFO | ✅ | testTheOldestUnexpiredPackageIsUsedFirst |
| ۱.۱۱ | valid_to هنگام خرید |
✅ | از validity_days لحظهٔ خرید |
| ۱.۱۲ | لغو → ردیف refund |
✅ | BookingService::cancel() |
| ۱.۱۳ | ارجاع به تسک ۱۳ برای سیاست بازگشت | ✅ | در docblock refund() |
| ۱.۱۴ | adjust فقط نقش مدیر و با reason |
✅ | منشی 403 |
| ۱.۱۵ | app:package:expire |
✅ | --dry-run هم دارد |
| ۱.۱۶ | قلاب PricingEngine |
✅ | patient_uuid اختیاری در quote |
| ۱.۱۷ | هشت endpoint | ✅ | ۹ تا: packages GET/POST · package/{uuid} GET/PATCH/DELETE · patient/{uuid}/package · patient/{uuid}/packages · patient-package/{uuid}/ledger · /adjust · /expire |
| ۱.۱۸ | TenantOwnershipChecker روی هر uuid |
✅ | testAnotherClinicCannotSeeOrTouchThePackage |
۲. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | چهار جدول | ✅ | Version20260731072023 |
| ۲.۲ | bigint روی هر دو ستون مبلغ |
✅ | |
| ۲.۳ | UNIQUE(appointment_id, kind) |
✅ | ⭐ |
| ۲.۴ | snapshot تعداد و قیمت | ✅ | قانون پنجم |
| ۲.۵ | ON DELETE RESTRICT روی سرویس و پکیج |
✅ | |
| ۲.۶ | package_services در AGGREGATE_CHILDREN |
✅ | |
| ۲.۷ | دفتر جفت tenant دارد | ✅ | ⭐ دلیلش در tenancy.md کنار کیف پول |
| ۲.۸ | TenantSchemaCoverageTest سبز |
✅ |
۳. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | PackagesPage با PriceInput و انتخاب سرویس |
✅ | + ورودی منوی تنظیمات |
| ۳.۲ | پکیجهای بیمار در PatientDetailPage |
✅ | تب «پکیجها» با مانده، انقضا، فروش و لینک دفتر |
| ۳.۳ | PatientPackageLedgerPage |
✅ | |
| ۳.۴ | ستون مانده تجمعی | ✅ | تصمیم ثبتشده در deviations.md — سرور محاسبهاش میکند (running_balance) نه UI — یک منبع، و همان عددی که تست بکاند تضمینش میکند |
| ۳.۵ | ستونهای دفتر | ✅ | «ثبتکننده» و «نوبت» هم اضافه شدند — دفترِ اصلاحپذیر بدون نام اصلاحکننده، نصف حسابرسی است. یادداشت قبلی: در جدول نمایش داده نمیشوند (عرض موبایل) |
| ۳.۶ | پیام «اعتبار تمام شده؛ نقدی محاسبه میشود» | ✅ | در تب پکیجهای بیمار |
| ۳.۷ | DataTable با skeleton و empty state |
✅ | |
| ۳.۸ | سرویسها با SearchableSelect |
✅ | هیچ <select> بومی |
| ۳.۹ | backTo روی زیرصفحهها |
✅ | |
| ۳.۱۰ | هیچ رنگ/شعاع hard-code | ✅ | |
| ۳.۱۱ | دارکمود و حالت فشرده | ✅ | اسکرینشات واقعی در دارکمود و حالت فشرده؛ ایرادی نماند |
| ۳.۱۲ | RTL و موبایل | ✅ | جدول دفتر اسکرول افقی داخلی دارد |
| ۳.۱۳ | formatRial و formatDate |
✅ | |
| ۳.۱۴ | وضعیت لیست در URL | ✅ | useUrlState در PackagesPage |
| ۳.۱۵ | همهٔ رشتهها فارسی | ✅ | |
| ۳.۱۶ | دکمهٔ adjust فقط برای مدیر |
✅ | can('appointment_settings', 'update') |
۴. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | SUM(delta) در همهٔ سناریوها، append-only |
✅ | ⭐ ترتیب purchase → consume → refund و ماندهٔ تجمعی |
| ۴.۲ | هیچ ستون مانده در schema | ✅ | ⭐⭐ |
| ۴.۳ | quote مصرف نمیکند |
✅ | ⭐⭐ دو quote پشتسرهم، مانده بیتغییر |
| ۴.۴ | مانده منفی نمیشود | ✅ | تصمیم ثبتشده در deviations.md — با مصرف پشتسرهم تست شد (testAnEmptyPackageIsSimplyNotApplied)؛ تست همزمانی واقعی با دو اتصال نوشته نشد |
| ۴.۵ | مصرف دوباره یک ردیف | ✅ | |
| ۴.۶ | FIFO | ✅ | |
| ۴.۷ | انقضا | ✅ | نمایش صفر + دستور + ردیف expiry |
| ۴.۸ | مجوز اصلاح | ✅ | منشی رد، مدیر بدون دلیل ۴۲۲، منفیکردن مانده ۴۲۲ |
| ۴.۹ | جداسازی محیط | ✅ | |
| ۴.۱۰ | یکپارچگی با قیمت | ✅ | final_rials صفر و ردیف package در breakdown |
| ۴.۱۱ | تست فرانت دفتر | ✅ | PatientPackageLedgerPage.test.tsx |
اجرا: ddev exec php bin/phpunit tests/Package → ۱۶ تست (۱ skip عمدی: تولید خروجی مستندات).
۵. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | docs/api/package.md |
✅ | JSON واقعی از اجرای واقعی |
| ۵.۲ | جدول مقایسهٔ قفل بدبینانه با سطل زمانی | ✅ | ⭐ در package.md |
| ۵.۳ | tenancy.md — تفاوت دفتر اعتبار با کیف پول |
✅ | ⭐ |
| ۵.۴ | یادداشت متقابل در pricing.md و appointment-booking.md |
✅ |
۶. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ✅ | ۵ مورد ⚠️ همه با دلیل |
| ۶.۲ | bin/phpunit کامل سبز |
✅ | ۱۲۶۷ تست |
| ۶.۳ | --group=slot-mode-frozen سبز |
✅ | |
| ۶.۴ | تستهای کیف پول سبز ماندند | ✅ | |
| ۶.۵ | phpstan بدون خطای جدید |
✅ | ۱۴ = baseline |
| ۶.۶ | npx tsc --noEmit و تستهای فرانت سبز |
✅ | ۶۳۰ تست (روی هاست؛ vitest داخل ddev اجرا نمیشود) |
| ۶.۷ | تستهای tenant سبز | ✅ | |
| ۶.۸ | docs/api/* بهروز |
✅ | |
| ۶.۹ | چکلیست UI کامل | ✅ | جز ۳.۱۱ |
| ۶.۱۰ | دو کلاینت دیگر بررسی شدند | ✅ | هر دو کلاینت با graphify بررسی شدند: هیچکدام quote را صدا نمیزنند. یادداشت قبلی: نمایش «مبلغ صفر» در nobat724_front دیده نشد — پکیج فعلاً فقط پنلمحور است |
| ۶.۱۱ | commit، سپس graphify update . |
✅ | دو کامیت جدا |
| ۶.۱۲ | موارد بهتعویق با دلیل | ✅ | سیاست بازگشت اعتبار → تسک ۱۳ · تست همزمانی واقعی (۴.۴) · بازبینی چشمی (۳.۱۱) |