# تسک ۱۱ — پکیج و دفتر اعتبار جلسات **فاز:** ۳ (کسب‌وکار) · **وابستگی:** ۰۸ · **زمان:** ۱۰-۱۲ ساعت --- ## هدف مستند بند ۱۲: «پکیج شش جلسه لیزر» حالت رایج کلینیک زیبایی است. بیمار یکجا پول می‌دهد و بعداً جلساتش را رزرو می‌کند. نکتهٔ فنی مستند: **اعتبار را به صورت دفتر حساب نگه می‌داریم، نه یک عدد شمارنده.** ## وضعیت فعلی هیچ مفهومی از پکیج وجود ندارد. ولی الگوی «دفتر حساب» از قبل در پروژه هست و **درست پیاده شده**: `WalletTransaction` + `getWalletBalance(user)` — موجودی از جمع تراکنش‌ها محاسبه می‌شود، نه از یک ستون شمارنده. همان الگو اینجا تکرار می‌شود. ⚠️ نکتهٔ tenancy: `wallet_transactions` عمداً `ENTITIES` است (پول مال شخص است) ولی هر ردیف `recorded_entity_*` دارد. دفتر اعتبار جلسه **متفاوت** است: اعتبار جلسهٔ لیزر در کلینیک الف در کلینیک ب معنا ندارد. پس جفت tenant واقعی می‌گیرد، نه انتساب. ## دامنه **هست:** - `Package` — تعریف پکیج (سرویس، تعداد جلسه، قیمت، اعتبار زمانی) - `PatientPackage` — پکیج خریداری‌شدهٔ یک بیمار - `SessionCreditLedger` — دفتر اعتبار: هر تراکنش یک ردیف - مصرف اعتبار در `confirm` نوبت، بازگشت در لغو - اتصال به `PricingEngine` مرحلهٔ ۴ (قلاب تسک ۰۸) **نیست:** پروتکل دوره و فاصلهٔ جلسات (تسک ۱۲)، سیاست لغو (تسک ۱۳). ## Endpoint ها | متد | مسیر | توضیح | |---|---|---| | GET/POST | `/api/v1/packages` | تعریف پکیج | | GET/PATCH/DELETE | `/api/v1/package/{uuid}` | | | POST | `/api/v1/patient/{uuid}/package` | فروش پکیج به بیمار | | GET | `/api/v1/patient/{uuid}/packages` | پکیج‌های بیمار + مانده | | GET | `/api/v1/patient-package/{uuid}/ledger` | دفتر تراکنش‌های اعتبار | | POST | `/api/v1/patient-package/{uuid}/adjust` | اصلاح دستی با دلیل (فقط مدیر) | | POST | `/api/v1/patient-package/{uuid}/expire` | ابطال دستی | ## معیار پذیرش - ✅ موفق: پکیج «۶ جلسه لیزر فول‌بادی» با قیمت تعریف می‌شود، به بیمار فروخته می‌شود → `GET /patient/{uuid}/packages` مانده `6` می‌دهد و دفتر یک ردیف `purchase +6` دارد. - ✅ موفق: ثبت نوبت لیزر برای همان بیمار → `PricingEngine` مرحلهٔ ۴ یک واحد کسر می‌کند، مبلغ نهایی صفر می‌شود، دفتر ردیف `consume -1` می‌گیرد، مانده `5`. - ✅ موفق: لغو همان نوبت → ردیف `refund +1`، مانده `6`. **ردیف `consume` حذف نمی‌شود.** - ✅ موفق (**دفتر، نه شمارنده**): مانده همیشه `SUM(delta)` است. یک تست باید ثابت کند هیچ ستون `remaining` یا `used_count` در schema وجود ندارد. - ✅ موفق: `POST /adjust` با دلیل → ردیف `adjustment` با `reason` و `created_by`. - ❌ خطا: ثبت نوبت با پکیجی که مانده‌اش صفر است → پکیج اعمال نمی‌شود، مبلغ کامل محاسبه می‌شود (نه خطا — بیمار می‌تواند نقدی بپردازد). - ❌ خطا: پکیج محیط الف روی نوبت محیط ب → `404`. - ❌ خطا: `adjust` با نقش منشی → `403`. - ⚠️ مرزی: پکیج منقضی‌شده (`valid_to` گذشته) → مانده در نمایش صفر می‌شود ولی دفتر دست‌نخورده می‌ماند؛ ردیف `expiry` با delta منفی برابر مانده ثبت می‌شود. - ⚠️ مرزی: دو نوبت هم‌زمان که هر دو آخرین اعتبار را می‌خواهند → یکی می‌گیرد، دیگری مبلغ کامل. **بدون منفی شدن مانده.** - ⚠️ مرزی: بیمار دو پکیج معتبر برای یک سرویس دارد → قدیمی‌ترِ منقضی‌نشده اول مصرف شود (FIFO). - ⚠️ مرزی: پکیجی که هیچ سرویسی به آن وصل نیست → `422` هنگام ساخت. ## خروجی - `src/Package/` - `assets/admin/pages/PackagesPage.tsx` + کارت پکیج در `PatientDetailPage.tsx` - `docs/api/package.md`