Files
clinicpro/docs/new_feture/taskes/task-11-package-credit-ledger/checklist.md
T
hamedandClaude Opus 5 ca9648732d feat(package): session packages backed by a credit ledger
"Six laser sessions" is the common case in an aesthetics clinic: the patient
pays once and books the sessions later.

Credit is a ledger, not a counter. No table has a remaining/used_count column
and a schema test enforces that — the balance is always SUM(delta) over
append-only rows, so every number a patient sees has a full history behind it.
Corrections are new rows, never edits.

- purchase / consume / refund / adjustment / expiry, each with a reason, an
  author and the appointment it belongs to
- consume happens in confirm(), never in quote(): if the preview consumed, a
  page refresh would cost the patient a session
- cancelling adds a refund row; the consume row stays
- FIFO across a patient's packages — the oldest is closest to expiring
- an empty package is not an error, it just does not apply and the patient pays
- adjust/expire need a doctor or clinic role, and adjust always needs a reason
- app:package:expire writes the closing row so "where did my 3 sessions go?"
  always has an answer

Consume takes a pessimistic lock on the one package row. That is the opposite
of task 07's slot buckets, and docs/api/package.md carries the table explaining
why, so nobody unifies them later.

Idempotency checks for an existing consume row before inserting rather than
catching the unique violation: in Doctrine that exception closes the
EntityManager and burns the rest of the request. The unique key stays as the
last line of defence.

Admin: PackagesPage, a packages tab on the patient record, and a ledger page
whose running-balance column shows where the final number came from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:11:03 +03:30

8.6 KiB
Raw Blame History

چک‌لیست — تسک ۱۱ (پکیج و دفتر اعتبار جلسات)

وضعیت کلی: تمام‌شده با انحراف‌های ثبت‌شده · آخرین بازبینی: ۱۴۰۵/۰۵/۰۹

قواعد: _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 ⚠️ با بررسی پیش از درج نه 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
۳.۴ ستون مانده تجمعی ⚠️ سرور محاسبه‌اش می‌کند (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 پشت‌سرهم، مانده بی‌تغییر
۴.۴ مانده منفی نمی‌شود ⚠️ با مصرف پشت‌سرهم تست شد (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 کامل جز ۳.۱۱
۶.۱۰ دو کلاینت دیگر بررسی شدند ⚠️ patient_uuid فیلد اختیاری تازه در quote است، پس قرارداد موجود نشکست؛ نمایش «مبلغ صفر» در nobat724_front دیده نشد — پکیج فعلاً فقط پنل‌محور است
۶.۱۱ commit، سپس graphify update . دو کامیت جدا
۶.۱۲ موارد به‌تعویق با دلیل سیاست بازگشت اعتبار → تسک ۱۳ · تست هم‌زمانی واقعی (۴.۴) · بازبینی چشمی (۳.۱۱)