Files
clinicpro/docs/api/subscription.md
T

265 lines
9.0 KiB
Markdown

# Subscription API
مدیریت پنل‌های اشتراکی (Free / Basic / Professional).
---
## GET /api/v1/subscription/plans
لیست پنل‌ها با دوره‌های فعال (عمومی — بدون auth).
> پلن `free`: همه امکانات (`patient_records`, `services`, `sms_panel`, `insurance`) فعال‌اند؛ محدودیت‌هایش عددی‌اند — تعداد منشی (`max_secretaries`) و تعداد منبع (`max_resources`).
> `max_resources` سقف منابع محیط است. مقدار `-1` یعنی نامحدود. مقادیر شیپ‌شده: `free` = ۱، `basic` = ۳، `professional` = `-1`. اجرای این سقف در `POST /api/v1/resource` است — [resource.md](resource.md).
**Response 200:**
```json
{
"success": true,
"data": [
{
"uuid": "...",
"name": "free",
"level": 0,
"max_secretaries": 1,
"max_resources": 1,
"features": { "patient_records": true, "services": true, "sms_panel": true, "insurance": true },
"active": true,
"periods": []
},
{
"uuid": "...",
"name": "basic",
"level": 1,
"max_secretaries": 3,
"max_resources": 3,
"features": { "patient_records": true, "services": true, "sms_panel": false },
"active": true,
"periods": [
{
"uuid": "...",
"plan_uuid": "...",
"label": "یک ماهه",
"duration_months": 1,
"price_rials": 290000,
"is_trial": false,
"active": true,
"sort_order": 1
}
]
}
]
}
```
---
## GET /api/v1/subscription/my
اشتراک فعال کاربر جاری.
**Permission:** `IS_AUTHENTICATED_FULLY`
**نکته:** از نسخه فعلی، این endpoint برای `ROLE_SECRETARY` نیز کار می‌کند. منشی از طریق `UserActiveContextRepository` به `db_uuid` entity مربوطه (doctor یا clinic) دسترسی پیدا می‌کند و اشتراک همان entity برگردانده می‌شود.
**Response 200:**
```json
{
"success": true,
"data": {
"subscription": {
"uuid": "...",
"plan": { "name": "basic", "level": 1, "max_secretaries": 3, "max_resources": 3, "features": {...} },
"period": { "label": "یک ماهه", "duration_months": 1, "price_rials": 290000 },
"is_trial": false,
"starts_at": 1718000000,
"expires_at": 1720678400,
"days_remaining": 30,
"is_active": true
},
"used_trial": false,
"effective_plan": { "name": "basic", "level": 1, "max_secretaries": 3, "max_resources": 3, "features": {...} }
}
}
```
اگر اشتراک فعالی نداشت `subscription` برابر `null` است، اما `effective_plan` همیشه مقدار دارد: پلن اشتراک فعال، یا در نبود اشتراک، **پلن پیش‌فرض `free`**. فرانت‌اند برای تعیین دسترسی به امکانات (`hasFeature`) باید از `effective_plan` استفاده کند (نه `subscription`) تا کاربرانِ بدون اشتراک هم امکانات پلن free را داشته باشند. `subscription`/`hasPlan` صرفاً برای نمایش وضعیت اشتراک پولی است.
### پاسخ کاهش‌یافته برای کاربرِ بدون مجوزِ `subscription.view` (2026-08)
پیش از این، منشیِ بدون این مجوز `403` می‌گرفت. نتیجه‌اش یک **قفلِ دروغین در پنل** بود:
سایدبار هر آیتم feature-دار (پروندهٔ بیماران، بیمه) را با `hasFeature()` گیت می‌کند و
بدون این پاسخ، `features` خالی می‌ماند و آیتم قفل و به صفحهٔ اشتراک هدایت می‌شد — حتی
وقتی خودِ API آن قابلیت را به همان منشی می‌داد.
حالا پاسخ `200` است ولی فقط توانمندی‌های پلن را دارد:
```json
{"success":true,"data":{
"subscription": null,
"used_trial": false,
"effective_plan": { "features": { "patient_records": true, "…": true }, "max_secretaries": 1, "max_resources": 1 }
}}
```
- `subscription`، `used_trial` و فیلدهای هویتی/سطحِ پلن (`name`, `level`, `uuid`, `active`,
`periods`) در این حالت **نمی‌آیند**.
- افشای تازه‌ای نیست: `GET /subscription/plans` عمومی است و همین `features` را (به‌همراه
قیمت‌ها) برای همهٔ پلن‌ها می‌دهد.
- سایر نقش‌ها و منشیِ دارای `subscription.view` همان پاسخ کامل بالا را می‌گیرند.
---
## POST /api/v1/subscription/trial
فعال‌سازی تریال رایگان (یکبار برای هر entity).
**Permission:** `IS_AUTHENTICATED_FULLY`
**Response 201:** همان ساختار ClinicSubscription
**Errors:**
| Code | HTTP | توضیح |
|------|------|-------|
| ERR_TRIAL_ALREADY_USED | 422 | قبلاً از تریال استفاده شده |
| ERR_TRIAL_DISABLED | 422 | تریال غیرفعال است — یا `SiteConfig: trial_enabled=0`، یا پلن `basic` هیچ دورهٔ تریالِ `active` ندارد |
| ERR_FORBIDDEN_001 | 403 | پروفایل doctor/clinic یافت نشد |
| ERR_NOT_FOUND_001 | 500 | پلن `basic` وجود ندارد یا غیرفعال است (نصب ناقص) |
---
## POST /api/v1/subscription-payment
شروع پرداخت اشتراک.
**Permission:** `IS_AUTHENTICATED_FULLY`
**Request Body:**
```json
{
"gateway": "mellat",
"amount_rials": 290000,
"period_uuid": "uuid-of-subscription-period",
"frontend_address": "https://example.com/payment-result"
}
```
| فیلد | نوع | الزامی |
|------|-----|--------|
| gateway | string (mellat\|sep) | ✅ |
| amount_rials | integer | ✅ |
| period_uuid | string (UUID) | ✅ |
| frontend_address | string (URL) | ❌ |
**Response 200:**
```json
{
"success": true,
"data": {
"payment_uuid": "...",
"redirect_url": "https://gateway.shaparak.ir/...",
"order_id": "ORD-XXXXXXXXXXXXXXXX"
}
}
```
---
## GET /api/v1/subscription-payment/callback/{gateway}
callback درگاه پرداخت — پس از پرداخت موفق، `ClinicSubscription` به صورت خودکار ایجاد می‌شود (بر اساس `period_uuid` ذخیره‌شده در metadata پرداخت).
---
## Admin Endpoints
### GET /api/v1/admin/subscription/plans
**Permission:** `ROLE_ADMIN` — لیست **همه** پلن‌ها شامل غیرفعال‌ها (بر خلاف endpoint عمومی که فقط فعال‌ها را برمی‌گرداند). هر پلن فیلد `active` دارد و `periods` همیشه یک آرایه است (فقط دوره‌های فعال).
### POST /api/v1/admin/subscription/plan
**Permission:** `ROLE_ADMIN`
```json
{
"name": "enterprise",
"level": 3,
"max_secretaries": 20,
"max_resources": -1,
"features": { "patient_records": true, "services": true, "sms_panel": true }
}
```
`max_resources` اختیاری است و پیش‌فرضش `1` — پلنِ ناشناخته نباید بی‌صدا نامحدود شود. مقدار `-1` یعنی نامحدود. در `PATCH` هم همین فیلد پذیرفته می‌شود.
**خطاها:**
| کد | HTTP | شرح |
|----|------|-----|
| ERR_VALIDATION_001 | 422 | `name` یا `level` ارسال نشده |
| ERR_VALIDATION_001 | 422 | پلنی با این نام از قبل وجود دارد (نام یکتاست) |
### PATCH /api/v1/admin/subscription/plan/{uuid}
**Permission:** `ROLE_ADMIN` — ویرایش پنل (همه فیلدها اختیاری)
**خطاها:**
| کد | HTTP | شرح |
|----|------|-----|
| ERR_SUBSCRIPTION_NOT_FOUND | 404 | پلن یافت نشد |
| ERR_VALIDATION_001 | 422 | نام جدید متعلق به پلن دیگری است |
### POST /api/v1/admin/subscription/period
**Permission:** `ROLE_ADMIN`
```json
{
"plan_uuid": "...",
"label": "شش ماهه",
"duration_months": 6,
"price_rials": 1500000,
"is_trial": false,
"sort_order": 2
}
```
### PATCH /api/v1/admin/subscription/period/{uuid}
**Permission:** `ROLE_ADMIN` — ویرایش دوره
### DELETE /api/v1/admin/subscription/period/{uuid}
**Permission:** `ROLE_ADMIN` — غیرفعال کردن دوره (soft delete: `active=false`)
### GET /api/v1/admin/subscription/report
**Permission:** `ROLE_ADMIN`
Query params: `page`, `limit`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"entity_type": "clinic",
"entity_id": 5,
"is_trial": false,
"starts_at": 1718000000,
"expires_at": 1720678400,
"plan_name": "basic",
"plan_level": 1
}
],
"meta": { "totalRecords": 50, "totalPages": 3, "currentPage": 1 }
}
```
---
## Error Codes
| Code | HTTP | توضیح |
|------|------|-------|
| ERR_SUBSCRIPTION_REQUIRED | 403 | قابلیت نیاز به پنل Basic+ دارد |
| ERR_TRIAL_ALREADY_USED | 422 | تریال قبلاً استفاده شده |
| ERR_TRIAL_DISABLED | 422 | تریال غیرفعال است |
| ERR_SUBSCRIPTION_NOT_FOUND | 404 | پنل یافت نشد |