# تسک ۱۱: پنل اشتراکی (Subscription Tiers) ## توضیح پیاده‌سازی سیستم پنل‌های اشتراکی سه‌سطحی. ادمین دوره‌ها و قیمت‌ها را تعریف می‌کند. این تسک **gate check** برای تسک‌های ۱۲، ۱۳، ۱۵ فراهم می‌کند. **نکته مهم:** `/api/v1/subscription-payment` و callback آن از قبل در `PaymentController` موجود است. در این تسک فقط باید بعد از callback موفق، `ClinicSubscription` ساخته شود + endpoint های Subscription خودش. ## سطوح پنل | Level | Name | منشی | patient_records | services | |-------|------|------|----------------|---------| | 0 | free | ۱ | ❌ | ❌ | | 1 | basic | ۲ | ✅ | ✅ | | 2 | professional | ۵ | ✅ | ✅ | ## Endpoint ها | متد | مسیر | Permission | توضیح | |-----|------|-----------|-------| | GET | `/api/v1/subscription/plans` | public | لیست پنل‌ها + دوره‌ها + قیمت | | GET | `/api/v1/subscription/my` | doctor/clinic | اشتراک فعال + `used_trial` | | POST | `/api/v1/subscription/trial` | doctor/clinic | فعال‌سازی تریال Basic (یک‌بار) | | GET | `/api/v1/admin/subscription/plans` | ROLE_ADMIN | لیست مدیریت پنل‌ها | | POST | `/api/v1/admin/subscription/plan` | ROLE_ADMIN | ایجاد پنل | | PATCH | `/api/v1/admin/subscription/plan/{uuid}` | ROLE_ADMIN | ویرایش پنل | | POST | `/api/v1/admin/subscription/period` | ROLE_ADMIN | افزودن دوره | | PATCH | `/api/v1/admin/subscription/period/{uuid}` | ROLE_ADMIN | ویرایش دوره/قیمت | | DELETE | `/api/v1/admin/subscription/period/{uuid}` | ROLE_ADMIN | غیرفعال‌سازی دوره | | GET | `/api/v1/admin/subscription/report` | ROLE_ADMIN | گزارش فروش + تریال‌ها | **موجود (تغییر نمی‌کند):** - `POST /api/v1/subscription-payment` — شروع پرداخت (body: `{ period_uuid }`) - `GET|POST /api/v1/subscription-payment/callback/{gateway}` — callback gateway ## پیش‌نیازها - تسک ۰۲ (Auth/JWT) - تسک ۱۵-payment (PaymentController — موجود) ## زمان تخمینی ۱۴ تا ۱۶ ساعت ## نمونه Request ### POST /api/v1/subscription/trial ```json {} ``` (body خالی — entity از JWT گرفته می‌شود) ### POST /api/v1/admin/subscription/period ```json { "plan_uuid": "uuid-of-basic-plan", "label": "۶ ماهه", "duration_months": 6, "price_rials": 1200000, "is_trial": false, "sort_order": 3 } ``` ### POST /api/v1/subscription-payment (موجود) ```json { "period_uuid": "uuid-of-selected-period", "gateway": "mellat" } ``` ## نمونه Response ### GET /api/v1/subscription/plans ```json { "success": true, "data": [ { "uuid": "...", "name": "basic", "level": 1, "max_secretaries": 2, "features": { "patient_records": true, "services": true, "sms_panel": false }, "periods": [ { "uuid": "...", "label": "تریال ۱ ماهه", "duration_months": 1, "price_rials": 0, "is_trial": true }, { "uuid": "...", "label": "۱ ماهه", "duration_months": 1, "price_rials": 250000, "is_trial": false }, { "uuid": "...", "label": "۶ ماهه", "duration_months": 6, "price_rials": 1200000, "is_trial": false } ] } ] } ``` ### GET /api/v1/subscription/my ```json { "success": true, "data": { "plan": { "name": "basic", "level": 1, "features": { "patient_records": true, "services": true } }, "period": { "label": "۶ ماهه", "duration_months": 6 }, "is_trial": false, "starts_at": 1718000000, "expires_at": 1733360000, "used_trial": true, "days_remaining": 42 } } ``` وقتی اشتراک فعال ندارد: ```json { "success": true, "data": { "plan": { "name": "free", "level": 0 }, "expires_at": null, "used_trial": false } } ``` ### POST /api/v1/subscription/trial (موفق) ```json { "success": true, "data": { "plan": "basic", "starts_at": 1718000000, "expires_at": 1720678400 } } ``` ### POST /api/v1/subscription/trial (خطا — قبلاً استفاده شده) ```json { "success": false, "errors": [{ "code": "ERR_TRIAL_ALREADY_USED", "message": "قبلاً از تریال استفاده کرده‌اید" }] } ``` ## قوانین تریال - فقط برای پنل **basic** (level=1) - هر entity یک‌بار — constraint `UNIQUE(entity_type, entity_id, is_trial)` + مقدار `is_trial=1` در DB - بدون پرداخت — `payment_id = null` - بعد از انقضا → برگشت به Free (داده‌ها حفظ می‌شوند) - ادمین می‌تواند تریال را کلاً غیرفعال کند: `SiteConfig.trial_enabled = false` ## قانون تمدید ``` expires_at جدید = max(expires_at فعلی, time()) + duration_months × 30 × 86400 ``` یعنی اگر اشتراک هنوز منقضی نشده، تمدید از تاریخ انقضا محاسبه می‌شود (نه از now). ## gate check در تسک‌های بعدی ```php // SubscriptionService::hasFeature('patient_records') → bool // false → $this->error(ErrorCodes::ERR_SUBSCRIPTION_REQUIRED, '...', 403) ```