# تسک ۱۱ — پکیج و دفتر اعتبار جلسات

**فاز:** ۳ (کسب‌وکار) · **وابستگی:** ۰۸ · **زمان:** ۱۰-۱۲ ساعت

---

## هدف

مستند بند ۱۲: «پکیج شش جلسه لیزر» حالت رایج کلینیک زیبایی است. بیمار یکجا پول می‌دهد و
بعداً جلساتش را رزرو می‌کند.

نکتهٔ فنی مستند: **اعتبار را به صورت دفتر حساب نگه می‌داریم، نه یک عدد شمارنده.**

## وضعیت فعلی

هیچ مفهومی از پکیج وجود ندارد. ولی الگوی «دفتر حساب» از قبل در پروژه هست و **درست
پیاده شده**: `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`
