feat: add admin subscription granting feature
- Implemented the ability for admins to grant subscriptions to doctors and clinics without payment. - Added new API endpoint `/api/v1/admin/subscription/grant` for granting subscriptions. - Updated the subscription model to track the admin who granted the subscription. - Enhanced the subscription report to include details about granted subscriptions. - Introduced a new `is_granted` field to indicate if a subscription was granted by an admin. - Updated the database schema to support the new functionality with a migration. - Added tests to ensure the correct behavior of the subscription granting process.
This commit is contained in:
+134
-8
@@ -72,6 +72,7 @@
|
||||
"plan": { "name": "basic", "level": 1, "max_secretaries": 3, "max_resources": 3, "features": {...} },
|
||||
"period": { "label": "یک ماهه", "duration_months": 1, "price_rials": 290000 },
|
||||
"is_trial": false,
|
||||
"is_granted": false,
|
||||
"starts_at": 1718000000,
|
||||
"expires_at": 1720678400,
|
||||
"days_remaining": 30,
|
||||
@@ -83,6 +84,8 @@
|
||||
}
|
||||
```
|
||||
|
||||
`is_granted` یعنی این اشتراک را ادمین بدون پرداخت اعطا کرده است.
|
||||
|
||||
اگر اشتراک فعالی نداشت `subscription` برابر `null` است، اما `effective_plan` همیشه مقدار دارد: پلن اشتراک فعال، یا در نبود اشتراک، **پلن پیشفرض `free`**. فرانتاند برای تعیین دسترسی به امکانات (`hasFeature`) باید از `effective_plan` استفاده کند (نه `subscription`) تا کاربرانِ بدون اشتراک هم امکانات پلن free را داشته باشند. `subscription`/`hasPlan` صرفاً برای نمایش وضعیت اشتراک پولی است.
|
||||
|
||||
### پاسخ کاهشیافته برای کاربرِ بدون مجوزِ `subscription.view` (2026-08)
|
||||
@@ -232,27 +235,150 @@ callback درگاه پرداخت — پس از پرداخت موفق، `ClinicSu
|
||||
### DELETE /api/v1/admin/subscription/period/{uuid}
|
||||
**Permission:** `ROLE_ADMIN` — غیرفعال کردن دوره (soft delete: `active=false`)
|
||||
|
||||
### POST /api/v1/admin/subscription/grant
|
||||
**Permission:** `ROLE_ADMIN` — اعطای اشتراک به یک پزشک یا کلینیک، بدون پرداخت
|
||||
|
||||
مقصد با `uuid` مشخص میشود، نه `id`؛ `id` داخلی است و در پاسخهای ادمین نمیآید.
|
||||
|
||||
اشتراکِ ساختهشده هرگز `is_trial` نمیگیرد، پس تریالِ استفادهنشدهٔ مقصد نمیسوزد.
|
||||
اگر مقصد اشتراک فعال داشته باشد، مدتِ دوره روی انقضای فعلی افزوده میشود، نه از امروز.
|
||||
|
||||
**Request**
|
||||
|
||||
| فیلد | نوع | الزامی | توضیح |
|
||||
|------|-----|--------|-------|
|
||||
| entity_type | string | بله | `doctor` یا `clinic` |
|
||||
| entity_uuid | string | بله | uuid پزشک یا کلینیک |
|
||||
| period_uuid | string | بله | uuid دورهٔ اشتراک؛ پلن از خود دوره خوانده میشود |
|
||||
|
||||
```json
|
||||
{
|
||||
"entity_type": "doctor",
|
||||
"entity_uuid": "44279545-9eab-4fc5-8b81-d04485ca38a7",
|
||||
"period_uuid": "72dfbf23-b4fc-4bb0-a6f7-abcb6441754b"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 201**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "2687342c-85fa-4610-aed9-bba42004f920",
|
||||
"plan": {
|
||||
"uuid": "6c2573e1-98e4-47e5-ba24-b0af92e55ffd",
|
||||
"name": "professional",
|
||||
"level": 2,
|
||||
"max_secretaries": 5,
|
||||
"max_resources": -1,
|
||||
"features": { "patient_records": true, "services": true, "sms_panel": true, "insurance": true },
|
||||
"active": true
|
||||
},
|
||||
"period": {
|
||||
"uuid": "72dfbf23-b4fc-4bb0-a6f7-abcb6441754b",
|
||||
"plan_uuid": "6c2573e1-98e4-47e5-ba24-b0af92e55ffd",
|
||||
"label": "یک ماهه",
|
||||
"duration_months": 1,
|
||||
"price_rials": 20000000,
|
||||
"is_trial": false,
|
||||
"active": true,
|
||||
"sort_order": 1
|
||||
},
|
||||
"is_trial": false,
|
||||
"is_granted": true,
|
||||
"starts_at": 1786268482,
|
||||
"expires_at": 1788860482,
|
||||
"days_remaining": 30,
|
||||
"is_active": true,
|
||||
"created_at": 1786268482
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**خطاها**
|
||||
|
||||
| وضعیت | کد | حالت |
|
||||
|-------|----|------|
|
||||
| 422 | ERR_VALIDATION_001 | `entity_type` غیر از `doctor`/`clinic`، یا نبودِ `entity_uuid`/`period_uuid` |
|
||||
| 404 | ERR_NOT_FOUND_001 | مقصد یافت نشد («مقصد اشتراک یافت نشد») |
|
||||
| 404 | ERR_NOT_FOUND_001 | دوره یافت نشد |
|
||||
| 401 | ERR_AUTH_001 | بدون توکن |
|
||||
| 403 | — | توکن معتبر ولی بدون `ROLE_ADMIN` |
|
||||
|
||||
> **هشدار downgrade:** `findActive` آخرین رکورد را بر اساس `id` برمیدارد، نه بالاترین
|
||||
> پلن. پس اعطای پلنی پایینتر از پلن فعال، عملاً پلن مؤثر مقصد را کاهش میدهد. پنل
|
||||
> ادمین قبل از ثبت این حالت تأیید میگیرد؛ خودِ endpoint جلوی آن را نمیگیرد.
|
||||
|
||||
### GET /api/v1/admin/subscription/active/{entityType}/{entityUuid}
|
||||
**Permission:** `ROLE_ADMIN` — اشتراک فعالِ یک مقصد، برای نمایش پیش از اعطا
|
||||
|
||||
`entityType` یکی از `doctor` یا `clinic`.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"subscription": {
|
||||
"uuid": "2687342c-85fa-4610-aed9-bba42004f920",
|
||||
"is_trial": false,
|
||||
"is_granted": true,
|
||||
"expires_at": 1788860482,
|
||||
"days_remaining": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
نبودِ اشتراک فعال با `"subscription": null` برمیگردد، نه ۴۰۴.
|
||||
|
||||
| وضعیت | کد | حالت |
|
||||
|-------|----|------|
|
||||
| 422 | ERR_VALIDATION_001 | `entityType` غیر از `doctor`/`clinic` |
|
||||
| 404 | ERR_NOT_FOUND_001 | مقصد یافت نشد |
|
||||
|
||||
### GET /api/v1/admin/subscription/report
|
||||
**Permission:** `ROLE_ADMIN`
|
||||
|
||||
Query params: `page`, `limit`
|
||||
Query params: `page`, `limit` — مقدار `limit` بین ۱۰ تا ۱۰۰ کلیپ میشود.
|
||||
|
||||
`isGranted` یعنی این اشتراک را ادمین بدون پرداخت داده و `grantedBy` نام یا شمارهٔ همان ادمین است.
|
||||
`payment` تنها معیارِ تشخیص نیست: اشتراک تریال هم پرداختی ندارد.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "...",
|
||||
"entity_type": "clinic",
|
||||
"entity_id": 5,
|
||||
"is_trial": false,
|
||||
"starts_at": 1718000000,
|
||||
"expires_at": 1720678400,
|
||||
"uuid": "2687342c-85fa-4610-aed9-bba42004f920",
|
||||
"entityType": "doctor",
|
||||
"entityId": 19545,
|
||||
"entityName": "پزشک دعوت شده2",
|
||||
"isTrial": false,
|
||||
"isGranted": true,
|
||||
"grantedBy": "ادمین",
|
||||
"startsAt": 1786268482,
|
||||
"expiresAt": 1788860482,
|
||||
"createdAt": 1786268482,
|
||||
"plan_name": "professional",
|
||||
"plan_level": 2
|
||||
},
|
||||
{
|
||||
"uuid": "ba1b8b92-f3d0-4cc6-8217-2dfc0ffe0d28",
|
||||
"entityType": "clinic",
|
||||
"entityId": 1,
|
||||
"entityName": "09398631203",
|
||||
"isTrial": true,
|
||||
"isGranted": false,
|
||||
"grantedBy": null,
|
||||
"startsAt": 1783093441,
|
||||
"expiresAt": 1785685441,
|
||||
"createdAt": 1783093441,
|
||||
"plan_name": "basic",
|
||||
"plan_level": 1
|
||||
}
|
||||
],
|
||||
"meta": { "totalRecords": 50, "totalPages": 3, "currentPage": 1 }
|
||||
"meta": { "totalRecords": 3, "totalPages": 1, "currentPage": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user