feat: implement staff management and subscription system
- Added StaffController for managing clinic staff, including listing, creating, updating, and toggling staff status. - Created ClinicStaff entity and repository for staff data handling. - Developed SubscriptionController to manage subscription plans and periods, including trial subscriptions. - Introduced SubscriptionPlan, SubscriptionPeriod, and ClinicSubscription entities for subscription management. - Implemented SubscriptionService for handling subscription logic, including trial activation and subscription creation from payments. - Added necessary repositories for subscription entities to facilitate data access and manipulation.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
# Clinic Services API
|
||||
|
||||
مدیریت بخشها و سرویسهای کلینیک/مطب.
|
||||
|
||||
**نیاز به پنل:** Basic یا بالاتر (`ERR_SUBSCRIPTION_REQUIRED` اگر نداشت)
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/service-sections
|
||||
|
||||
لیست بخشهای سرویس entity جاری.
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY` + پنل Basic+
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "...",
|
||||
"entity_type": "clinic",
|
||||
"entity_id": 5,
|
||||
"name": "آزمایشگاه",
|
||||
"active": true,
|
||||
"created_at": 1718000000,
|
||||
"updated_at": 1718000000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/service-section
|
||||
|
||||
ایجاد بخش جدید.
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY` + پنل Basic+
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{ "name": "رادیولوژی" }
|
||||
```
|
||||
|
||||
**Response 201:** ServiceSection object
|
||||
|
||||
**Errors:**
|
||||
| Code | HTTP | توضیح |
|
||||
|------|------|-------|
|
||||
| ERR_SUBSCRIPTION_REQUIRED | 403 | نیاز به پنل Basic+ |
|
||||
| ERR_VALIDATION_001 | 422 | name خالی است |
|
||||
|
||||
---
|
||||
|
||||
## PATCH /api/v1/service-section/{uuid}
|
||||
|
||||
ویرایش بخش.
|
||||
|
||||
**Permission:** owner یا ROLE_ADMIN
|
||||
|
||||
```json
|
||||
{ "name": "رادیولوژی دیجیتال", "active": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DELETE /api/v1/service-section/{uuid}
|
||||
|
||||
حذف بخش (cascade — همه ServiceItem های آن حذف میشوند).
|
||||
|
||||
**Permission:** owner
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/service-items/{sectionUuid}
|
||||
|
||||
لیست سرویسهای یک بخش.
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "...",
|
||||
"section_uuid": "...",
|
||||
"staff_uuid": "...",
|
||||
"staff_name": "علی محمدی",
|
||||
"name": "رادیوگرافی مستقیم",
|
||||
"price_rials": 500000,
|
||||
"active": true,
|
||||
"created_at": 1718000000,
|
||||
"updated_at": 1718000000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/service-item
|
||||
|
||||
ایجاد سرویس جدید.
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY` + پنل Basic+
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"section_uuid": "...",
|
||||
"name": "رادیوگرافی مستقیم",
|
||||
"price_rials": 500000,
|
||||
"staff_uuid": "..."
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | الزامی |
|
||||
|------|-----|--------|
|
||||
| section_uuid | UUID | ✅ |
|
||||
| name | string | ✅ |
|
||||
| price_rials | integer | ❌ (پیشفرض 0) |
|
||||
| staff_uuid | UUID | ❌ |
|
||||
|
||||
**Response 201:** ServiceItem object
|
||||
|
||||
---
|
||||
|
||||
## PATCH /api/v1/service-item/{uuid}
|
||||
|
||||
ویرایش سرویس.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "رادیوگرافی دیجیتال",
|
||||
"price_rials": 600000,
|
||||
"staff_uuid": null,
|
||||
"active": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DELETE /api/v1/service-item/{uuid}
|
||||
|
||||
حذف سرویس.
|
||||
|
||||
اگر سرویس در پرونده بیماری استفاده شده باشد، خطا برمیگرداند:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"errors": [{ "code": "ERR_SERVICE_ITEM_IN_USE", "message": "این سرویس در پرونده بیمار ثبت شده است" }]
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
| Code | HTTP | توضیح |
|
||||
|------|------|-------|
|
||||
| ERR_SUBSCRIPTION_REQUIRED | 403 | نیاز به پنل Basic+ |
|
||||
| ERR_SERVICE_NOT_FOUND | 404 | سرویس یافت نشد |
|
||||
| ERR_SERVICE_ITEM_IN_USE | 409 | سرویس در پرونده بیمار استفاده شده |
|
||||
+83
-9
@@ -10,6 +10,13 @@ Returns stats and today's schedule for the authenticated clinic owner.
|
||||
|
||||
**Auth:** `ROLE_CLINIC` required
|
||||
|
||||
### Query params
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `from` | int (unix) | start of current month | Period start for patient/revenue stats |
|
||||
| `to` | int (unix) | now | Period end for patient/revenue stats |
|
||||
|
||||
### Response `200`
|
||||
|
||||
```json
|
||||
@@ -26,8 +33,12 @@ Returns stats and today's schedule for the authenticated clinic owner.
|
||||
"total_doctors": 5,
|
||||
"today_appointments": 12,
|
||||
"this_month_appointments": 87,
|
||||
"pending_invitations": 2
|
||||
"pending_invitations": 2,
|
||||
"sms_wallet_balance": 50000,
|
||||
"unique_patients_count": 34,
|
||||
"revenue_period_rials": 12500000
|
||||
},
|
||||
"period": { "from": 1717200000, "to": 1719792000 },
|
||||
"today_appointments": [
|
||||
{
|
||||
"uuid": "string",
|
||||
@@ -48,8 +59,12 @@ Returns stats and today's schedule for the authenticated clinic owner.
|
||||
}
|
||||
```
|
||||
|
||||
`today_appointments` — up to 5 records, ordered by `slot_start ASC`.
|
||||
`doctors` — all doctors belonging to this clinic; each includes their appointment count for today.
|
||||
**Field notes:**
|
||||
- `sms_wallet_balance` — current SMS wallet balance in Rials (0 if wallet not yet created)
|
||||
- `unique_patients_count` — distinct patients with at least one session in the `from`–`to` period
|
||||
- `revenue_period_rials` — sum of `final_price_rials` from all patient sessions in the period
|
||||
- `today_appointments` — up to 5 records, ordered by `slot_start ASC`
|
||||
- `doctors` — all doctors belonging to this clinic; each includes their appointment count for today
|
||||
|
||||
### Errors
|
||||
|
||||
@@ -65,6 +80,13 @@ Returns stats and today's schedule for the authenticated doctor.
|
||||
|
||||
**Auth:** `ROLE_DOCTOR` required
|
||||
|
||||
### Query params
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `from` | int (unix) | start of current month | Period start for patient/revenue stats |
|
||||
| `to` | int (unix) | now | Period end for patient/revenue stats |
|
||||
|
||||
### Response `200`
|
||||
|
||||
```json
|
||||
@@ -81,8 +103,12 @@ Returns stats and today's schedule for the authenticated doctor.
|
||||
"tomorrow_appointments": 5,
|
||||
"this_month_appointments": 62,
|
||||
"avg_rating": 4.6,
|
||||
"total_ratings": 34
|
||||
"total_ratings": 34,
|
||||
"sms_wallet_balance": 25000,
|
||||
"unique_patients_count": 18,
|
||||
"revenue_period_rials": 6800000
|
||||
},
|
||||
"period": { "from": 1717200000, "to": 1719792000 },
|
||||
"today_appointments": [
|
||||
{
|
||||
"uuid": "string",
|
||||
@@ -103,9 +129,11 @@ Returns stats and today's schedule for the authenticated doctor.
|
||||
}
|
||||
```
|
||||
|
||||
`today_appointments` — up to 10 records, ordered by `slot_start ASC`.
|
||||
`avg_rating` — rounded to 1 decimal; `null` if no ratings yet.
|
||||
`clinics` — all clinics the doctor belongs to.
|
||||
**Field notes:**
|
||||
- `today_appointments` — up to 10 records, ordered by `slot_start ASC`
|
||||
- `avg_rating` — rounded to 1 decimal; `null` if no ratings yet
|
||||
- `clinics` — all clinics the doctor belongs to
|
||||
- `sms_wallet_balance`, `unique_patients_count`, `revenue_period_rials` — same semantics as clinic dashboard
|
||||
|
||||
### Errors
|
||||
|
||||
@@ -157,11 +185,57 @@ Returns stats for the authenticated secretary and (conditionally) today's appoin
|
||||
}
|
||||
```
|
||||
|
||||
`today_appointments` — only populated when `permissions.resources.appointments.view === true`; otherwise empty array.
|
||||
`today_appointments` — up to 10 records when visible.
|
||||
`today_appointments` — only populated when `permissions.resources.appointments.view === true`; up to 10 records when visible.
|
||||
|
||||
### Errors
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_FORBIDDEN_001` | 403 | Secretary relation not configured or inactive |
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/admin/dashboard/charts
|
||||
|
||||
Returns time-series chart data for admin dashboard. All series are filtered to the given `from`–`to` window.
|
||||
|
||||
**Auth:** `ROLE_ADMIN` required
|
||||
|
||||
### Query params
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `from` | int (unix) | 30 days ago | Period start |
|
||||
| `to` | int (unix) | now | Period end |
|
||||
|
||||
### Response `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"appointments_by_day": [
|
||||
{ "date": "06/01", "count": 12 }
|
||||
],
|
||||
"revenue_by_day": [
|
||||
{ "date": "06/01", "amount": 3500000 }
|
||||
],
|
||||
"appointment_status": [
|
||||
{ "status": "confirmed", "count": 320 }
|
||||
],
|
||||
"top_specialties": [
|
||||
{ "name": "قلب و عروق", "count": 85 }
|
||||
],
|
||||
"subscription_sales_by_plan": [
|
||||
{ "plan": "basic", "count": 14, "revenue": 4060000 }
|
||||
],
|
||||
"period": { "from": 1717200000, "to": 1719792000 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Field notes:**
|
||||
- `appointments_by_day` / `revenue_by_day` — one entry per calendar day in the period; days with no data appear as `count: 0` / `amount: 0`
|
||||
- `appointment_status` — all-time counts, not filtered by period
|
||||
- `top_specialties` — top 8 by appointment volume, all-time
|
||||
- `subscription_sales_by_plan` — subscriptions created in period, grouped by plan; `revenue` sums only payments with status `received`
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
# Patient Records & Sessions API
|
||||
|
||||
## Overview
|
||||
|
||||
Patient records track patients per entity (doctor or clinic). Each record holds multiple sessions (visits). Access requires an active subscription with the `patient_records` feature.
|
||||
|
||||
**Base path:** `/api/v1`
|
||||
**Auth:** Bearer JWT (doctor or clinic role required)
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### List Patients
|
||||
|
||||
```
|
||||
GET /api/v1/patients
|
||||
```
|
||||
|
||||
Returns a paginated list of patient records belonging to the authenticated entity.
|
||||
|
||||
**Query params:**
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `page` | int | 1 | Page number |
|
||||
| `limit` | int | 20 | Items per page (10–50) |
|
||||
| `search` | string | — | Search by patient name or phone |
|
||||
|
||||
**Response 200:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "...",
|
||||
"entity_type": "doctor",
|
||||
"entity_id": 5,
|
||||
"user": { "uuid": "...", "fullName": "علی رضایی", "phone": "09123456789" },
|
||||
"created_by_type": "doctor",
|
||||
"created_by_id": 5,
|
||||
"created_at": 1718375000
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"totalRecords": 42,
|
||||
"totalPages": 3,
|
||||
"currentPage": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No active plan with `patient_records` feature |
|
||||
|
||||
---
|
||||
|
||||
### Create Patient Record
|
||||
|
||||
```
|
||||
POST /api/v1/patient
|
||||
```
|
||||
|
||||
Creates a patient record for a user under the current entity. If the record already exists, returns the existing record (idempotent).
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"user_uuid": "string (required)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 201:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "...",
|
||||
"entity_type": "doctor",
|
||||
"entity_id": 5,
|
||||
"user": { "uuid": "...", "fullName": "...", "phone": "..." },
|
||||
"created_by_type": "doctor",
|
||||
"created_by_id": 5,
|
||||
"created_at": 1718375000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_VALIDATION_001` | 422 | `user_uuid` missing |
|
||||
| `ERR_NOT_FOUND_001` | 404 | User not found |
|
||||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||||
|
||||
---
|
||||
|
||||
### Get Patient Record
|
||||
|
||||
```
|
||||
GET /api/v1/patient/{uuid}
|
||||
```
|
||||
|
||||
Returns a single patient record.
|
||||
|
||||
**Response 200:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "...",
|
||||
"entity_type": "doctor",
|
||||
"entity_id": 5,
|
||||
"user": { "uuid": "...", "fullName": "...", "phone": "..." },
|
||||
"created_by_type": "doctor",
|
||||
"created_by_id": 5,
|
||||
"created_at": 1718375000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_PATIENT_NOT_FOUND` | 404 | Record not found or not owned by caller |
|
||||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||||
|
||||
---
|
||||
|
||||
### List Patient Sessions
|
||||
|
||||
```
|
||||
GET /api/v1/patient/{uuid}/sessions
|
||||
```
|
||||
|
||||
Returns paginated sessions for a patient record.
|
||||
|
||||
**Query params:** `page`, `limit` (same as list)
|
||||
|
||||
**Response 200:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "...",
|
||||
"record_uuid": "...",
|
||||
"appointment_uuid": null,
|
||||
"insurance_base_id": null,
|
||||
"insurance_supplementary_id": null,
|
||||
"visit_price_rials": 200000,
|
||||
"base_insurance_discount_percent": "10.00",
|
||||
"supplementary_discount_percent": "5.00",
|
||||
"services_total_rials": 50000,
|
||||
"final_price_rials": 230000,
|
||||
"payment_method": "cash",
|
||||
"notes": "...",
|
||||
"created_at": 1718375000,
|
||||
"updated_at": 1718375000
|
||||
}
|
||||
],
|
||||
"meta": { "totalRecords": 8, "totalPages": 1, "currentPage": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_PATIENT_NOT_FOUND` | 404 | Record not found or not owned by caller |
|
||||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||||
|
||||
---
|
||||
|
||||
### Create Session
|
||||
|
||||
```
|
||||
POST /api/v1/patient/{uuid}/session
|
||||
```
|
||||
|
||||
Creates a new visit session for a patient record.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"visit_price_rials": 200000,
|
||||
"base_insurance_discount_percent": 10,
|
||||
"supplementary_discount_percent": 5,
|
||||
"insurance_base_id": null,
|
||||
"insurance_supplementary_id": null,
|
||||
"payment_method": "cash",
|
||||
"notes": "...",
|
||||
"services": [
|
||||
{
|
||||
"service_item_uuid": "...",
|
||||
"staff_uuid": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Field notes:**
|
||||
|
||||
- `payment_method`: `cash` | `card` | `insurance` | `online` | `pending`
|
||||
- `services`: array of service items to attach; `price_rials` is snapshot-copied from ServiceItem
|
||||
- `final_price_rials` is computed: `(visit_price × (1 - base%) × (1 - supp%)) + services_total`
|
||||
|
||||
**Response 201:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ...session object... }
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_PATIENT_NOT_FOUND` | 404 | Record not found or not owned |
|
||||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||||
|
||||
---
|
||||
|
||||
### Update Session
|
||||
|
||||
```
|
||||
PATCH /api/v1/session/{uuid}
|
||||
```
|
||||
|
||||
Updates mutable fields on a session.
|
||||
|
||||
**Request body (all optional):**
|
||||
|
||||
```json
|
||||
{
|
||||
"notes": "...",
|
||||
"payment_method": "card"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ...session object... }
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_SESSION_NOT_FOUND` | 404 | Session not found or not owned |
|
||||
|
||||
---
|
||||
|
||||
## Auto-Creation on Appointment Confirm
|
||||
|
||||
When an appointment's status changes to `confirmed` via `PATCH /api/v1/appointment/{uuid}/status`, the system automatically:
|
||||
|
||||
1. Creates a `PatientRecord` for the appointment's user (if not already existing) under the doctor entity
|
||||
2. Creates a blank `PatientSession` linked to the appointment
|
||||
|
||||
This only runs if the doctor has the `patient_records` subscription feature active.
|
||||
@@ -218,3 +218,31 @@ Get all secretaries for a specific doctor.
|
||||
| `ERR_AUTH_001` | 401 | Missing token |
|
||||
| `ERR_FORBIDDEN_001` | 403 | Not the doctor |
|
||||
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
|
||||
|
||||
---
|
||||
|
||||
## محدودیت پنل اشتراکی
|
||||
|
||||
تعداد منشیهای مجاز بر اساس پنل فعال doctor تعیین میشود:
|
||||
|
||||
| پنل | حداکثر منشی |
|
||||
|-----|-------------|
|
||||
| Free (بدون اشتراک) | ۱ |
|
||||
| Basic | ۳ |
|
||||
| Professional | ۱۰ |
|
||||
|
||||
اگر تعداد منشیهای فعال به حد مجاز رسیده باشد، ایجاد منشی جدید خطای زیر را برمیگرداند:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "ERR_SECRETARY_001",
|
||||
"message": "پلن فعلی اجازه منشی بیشتر را نمیدهد"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
برای افزایش محدودیت، باید پنل را از `POST /api/v1/subscription/trial` (تریال) یا `POST /api/v1/subscription-payment` (پرداخت) ارتقاء داد.
|
||||
|
||||
+117
@@ -298,3 +298,120 @@ Updated template with `status: "rejected"`.
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_VALIDATION_002` | 422 | Missing note |
|
||||
|
||||
---
|
||||
|
||||
## SMS Wallet
|
||||
|
||||
کیف پیامکی — جدا از کیف مالی، فقط برای ارسال پیامک.
|
||||
|
||||
### GET /api/v1/sms/wallet/balance
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"balance_rials": 15000,
|
||||
"sms_price_rials": 500,
|
||||
"estimated_sms_count": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/sms/wallet/charge
|
||||
|
||||
شارژ کیف پیامکی از طریق درگاه پرداخت.
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY`
|
||||
|
||||
```json
|
||||
{
|
||||
"gateway": "mellat",
|
||||
"amount_rials": 50000,
|
||||
"frontend_address": "https://example.com/sms-wallet"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"payment_uuid": "...",
|
||||
"redirect_url": "https://gateway...",
|
||||
"order_id": "ORD-..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
پس از پرداخت موفق، موجودی کیف خودکار شارژ میشود.
|
||||
|
||||
### GET /api/v1/sms/wallet/logs
|
||||
|
||||
تراکنشهای کیف پیامک (paginated).
|
||||
|
||||
**Query params:** `page`, `limit`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "...",
|
||||
"type": "credit",
|
||||
"amount_rials": 50000,
|
||||
"description": "شارژ کیف پیامک",
|
||||
"created_at": 1718000000
|
||||
}
|
||||
],
|
||||
"meta": { "totalRecords": 5, "totalPages": 1, "currentPage": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SMS Settings
|
||||
|
||||
### GET /api/v1/sms/settings
|
||||
|
||||
تنظیمات پیامک entity جاری.
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"entity_type": "clinic",
|
||||
"entity_id": 5,
|
||||
"reminder_enabled": true,
|
||||
"reminder_hours_before": 2,
|
||||
"post_visit_enabled": false,
|
||||
"post_visit_text": null,
|
||||
"updated_at": 1718000000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PATCH /api/v1/sms/settings
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY`
|
||||
|
||||
```json
|
||||
{
|
||||
"reminder_enabled": true,
|
||||
"reminder_hours_before": 3,
|
||||
"post_visit_enabled": true,
|
||||
"post_visit_text": "از مراجعه شما سپاسگزاریم"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Admin Endpoints
|
||||
|
||||
### GET /api/v1/admin/sms/wallet-report
|
||||
|
||||
**Permission:** `ROLE_ADMIN` — لیست همه کیفهای پیامکی (paginated)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# Staff API
|
||||
|
||||
مدیریت پرسنل مطب/کلینیک (بدون حذف — فقط toggle فعال/غیرفعال).
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/staff
|
||||
|
||||
دریافت لیست پرسنل entity جاری (از JWT).
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY` (ROLE_DOCTOR یا ROLE_CLINIC)
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "a1b2c3d4-...",
|
||||
"entity_type": "clinic",
|
||||
"entity_id": 5,
|
||||
"full_name": "علی محمدی",
|
||||
"phone": "09121234567",
|
||||
"job_title": "منشی",
|
||||
"address": null,
|
||||
"national_code": "0012345678",
|
||||
"active": true,
|
||||
"created_at": 1718000000,
|
||||
"updated_at": 1718000000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/staff
|
||||
|
||||
ایجاد پرسنل جدید.
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY` (ROLE_DOCTOR یا ROLE_CLINIC)
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"full_name": "علی محمدی",
|
||||
"phone": "09121234567",
|
||||
"job_title": "منشی",
|
||||
"address": "تهران، خیابان ولیعصر",
|
||||
"national_code": "0012345678"
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | الزامی |
|
||||
|------|-----|--------|
|
||||
| full_name | string | ✅ |
|
||||
| phone | string | ❌ |
|
||||
| job_title | string | ❌ |
|
||||
| address | string | ❌ |
|
||||
| national_code | string(10) | ❌ |
|
||||
|
||||
**Response 201:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "a1b2c3d4-...",
|
||||
"entity_type": "clinic",
|
||||
"entity_id": 5,
|
||||
"full_name": "علی محمدی",
|
||||
"phone": "09121234567",
|
||||
"job_title": "منشی",
|
||||
"address": "تهران، خیابان ولیعصر",
|
||||
"national_code": "0012345678",
|
||||
"active": true,
|
||||
"created_at": 1718000000,
|
||||
"updated_at": 1718000000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
| Code | HTTP | توضیح |
|
||||
|------|------|-------|
|
||||
| ERR_VALIDATION_001 | 422 | full_name خالی است |
|
||||
| ERR_FORBIDDEN_001 | 403 | پروفایل doctor/clinic یافت نشد |
|
||||
|
||||
---
|
||||
|
||||
## PATCH /api/v1/staff/{uuid}
|
||||
|
||||
ویرایش اطلاعات پرسنل.
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY` — فقط owner یا ROLE_ADMIN
|
||||
|
||||
**Request Body (همه فیلدها اختیاری):**
|
||||
```json
|
||||
{
|
||||
"full_name": "علی محمدی ویرایششده",
|
||||
"phone": "09129999999",
|
||||
"job_title": "منشی ارشد",
|
||||
"address": null,
|
||||
"national_code": null
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200:** همان ساختار staff object
|
||||
|
||||
**Errors:**
|
||||
| Code | HTTP | توضیح |
|
||||
|------|------|-------|
|
||||
| ERR_STAFF_NOT_FOUND | 404 | پرسنل یافت نشد |
|
||||
| ERR_FORBIDDEN_001 | 403 | دسترسی ندارید |
|
||||
|
||||
---
|
||||
|
||||
## PATCH /api/v1/staff/{uuid}/toggle
|
||||
|
||||
تغییر وضعیت فعال/غیرفعال پرسنل (soft toggle — هیچ حذفی انجام نمیشود).
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY` — فقط owner یا ROLE_ADMIN
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "a1b2c3d4-...",
|
||||
"active": false,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
| Code | HTTP | توضیح |
|
||||
|------|------|-------|
|
||||
| ERR_STAFF_NOT_FOUND | 404 | پرسنل یافت نشد |
|
||||
| ERR_FORBIDDEN_001 | 403 | دسترسی ندارید |
|
||||
@@ -0,0 +1,214 @@
|
||||
# Subscription API
|
||||
|
||||
مدیریت پنلهای اشتراکی (Free / Basic / Professional).
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/subscription/plans
|
||||
|
||||
لیست پنلها با دورههای فعال (عمومی — بدون auth).
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "...",
|
||||
"name": "free",
|
||||
"level": 0,
|
||||
"max_secretaries": 1,
|
||||
"features": { "patient_records": false, "services": false, "sms_panel": false },
|
||||
"active": true,
|
||||
"periods": []
|
||||
},
|
||||
{
|
||||
"uuid": "...",
|
||||
"name": "basic",
|
||||
"level": 1,
|
||||
"max_secretaries": 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`
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"subscription": {
|
||||
"uuid": "...",
|
||||
"plan": { "name": "basic", "level": 1, "max_secretaries": 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
اگر اشتراک فعالی نداشت `subscription` برابر `null` است.
|
||||
|
||||
---
|
||||
|
||||
## 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) |
|
||||
| ERR_FORBIDDEN_001 | 403 | پروفایل doctor/clinic یافت نشد |
|
||||
|
||||
---
|
||||
|
||||
## 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` — لیست همه پنلها
|
||||
|
||||
### POST /api/v1/admin/subscription/plan
|
||||
**Permission:** `ROLE_ADMIN`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "enterprise",
|
||||
"level": 3,
|
||||
"max_secretaries": 20,
|
||||
"features": { "patient_records": true, "services": true, "sms_panel": true }
|
||||
}
|
||||
```
|
||||
|
||||
### PATCH /api/v1/admin/subscription/plan/{uuid}
|
||||
**Permission:** `ROLE_ADMIN` — ویرایش پنل (همه فیلدها اختیاری)
|
||||
|
||||
### 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 | پنل یافت نشد |
|
||||
Reference in New Issue
Block a user