From cced85456adf3f664670eb94f56d4e3bd73fffdb Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 11 Jun 2026 10:27:27 +0330 Subject: [PATCH] Add API documentation for Representation, Secretary, Settlement, SMS, Specialty, Tag, and User Profile endpoints --- CLAUDE.md | 23 ++ docs/api/README.md | 90 ++++ docs/api/admin.md | 679 +++++++++++++++++++++++++++++++ docs/api/appointment-settings.md | 540 ++++++++++++++++++++++++ docs/api/appointment.md | 277 +++++++++++++ docs/api/auth.md | 284 +++++++++++++ docs/api/blog.md | 232 +++++++++++ docs/api/clinic-invitation.md | 312 ++++++++++++++ docs/api/clinic.md | 310 ++++++++++++++ docs/api/doctor-service.md | 146 +++++++ docs/api/doctor.md | 380 +++++++++++++++++ docs/api/insurance.md | 267 ++++++++++++ docs/api/location.md | 249 ++++++++++++ docs/api/payment.md | 178 ++++++++ docs/api/rating.md | 275 +++++++++++++ docs/api/representation.md | 227 +++++++++++ docs/api/secretary.md | 220 ++++++++++ docs/api/settlement.md | 254 ++++++++++++ docs/api/sms.md | 300 ++++++++++++++ docs/api/specialty.md | 162 ++++++++ docs/api/tag.md | 132 ++++++ docs/api/user-profile.md | 154 +++++++ 22 files changed, 5691 insertions(+) create mode 100644 docs/api/README.md create mode 100644 docs/api/admin.md create mode 100644 docs/api/appointment-settings.md create mode 100644 docs/api/appointment.md create mode 100644 docs/api/auth.md create mode 100644 docs/api/blog.md create mode 100644 docs/api/clinic-invitation.md create mode 100644 docs/api/clinic.md create mode 100644 docs/api/doctor-service.md create mode 100644 docs/api/doctor.md create mode 100644 docs/api/insurance.md create mode 100644 docs/api/location.md create mode 100644 docs/api/payment.md create mode 100644 docs/api/rating.md create mode 100644 docs/api/representation.md create mode 100644 docs/api/secretary.md create mode 100644 docs/api/settlement.md create mode 100644 docs/api/sms.md create mode 100644 docs/api/specialty.md create mode 100644 docs/api/tag.md create mode 100644 docs/api/user-profile.md diff --git a/CLAUDE.md b/CLAUDE.md index 32cfa7c2..f97ef0a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,3 +134,26 @@ City IDs (integer FK to `categories.id` where `bundle='city'`) are stored on ent 5. Add UI components: ``, ``, ``, `` **Category API endpoint pattern:** `GET /api/v1/categorys/{bundle}` (note: typo `categorys` is intentional — existing route). Response is double-nested: extract array with `data?.data?.data ?? []`. + +--- + +## Standing Rule — API Documentation + +**Whenever any API endpoint is created or modified** (controller file, route, request/response structure, error code, permission), the corresponding file in `docs/api/` **must be updated in the same session**. + +| Changed file | Doc to update | +|---|---| +| `src/Auth/*` | `docs/api/auth.md` | +| `src/Doctor/*` | `docs/api/doctor.md` | +| `src/Clinic/*` | `docs/api/clinic.md` + `docs/api/clinic-invitation.md` | +| `src/Appointment/Controller/AppointmentController.php` | `docs/api/appointment.md` | +| `src/Appointment/Controller/AppointmentSettings*` | `docs/api/appointment-settings.md` | +| `src/Payment/*` | `docs/api/payment.md` | +| `src/Settlement/*` | `docs/api/settlement.md` | +| `src/Rating/*` | `docs/api/rating.md` | +| `src/Secretary/*` | `docs/api/secretary.md` | +| `src/Representation/*` | `docs/api/representation.md` | +| `src/Sms/*` | `docs/api/sms.md` | +| `src/Blog/*` | `docs/api/blog.md` | +| `src/Admin/*` | `docs/api/admin.md` | +| Category/Province/City controllers | `docs/api/location.md`, `docs/api/specialty.md`, `docs/api/insurance.md`, `docs/api/doctor-service.md`, `docs/api/tag.md` | diff --git a/docs/api/README.md b/docs/api/README.md new file mode 100644 index 00000000..b38f4b1c --- /dev/null +++ b/docs/api/README.md @@ -0,0 +1,90 @@ +# ClinicPro — API Documentation Index + +> **Base URL:** `https://clinic-pro.ddev.site` +> **API Prefix:** `/api/v1` +> **Swagger UI:** `https://clinic-pro.ddev.site/api/doc` — user: `admin` / pass: `clinic123` + +--- + +## Authentication + +All protected endpoints require: +``` +Authorization: Bearer +``` + +| Role | Description | +|------|-------------| +| `PUBLIC` | No token required | +| `AUTH` | Any valid JWT | +| `ROLE_ADMIN` | Admin user | +| `ROLE_DOCTOR` | Doctor user | +| `ROLE_CLINIC` | Clinic owner | +| `ROLE_SECRETARY` | Secretary | + +--- + +## Standard Response Envelope + +```json +// Success +{ "success": true, "data": { ... } } + +// Paginated +{ "success": true, "data": [...], "meta": { "totalRecords": 100, "totalPages": 5, "currentPage": 1 } } + +// Error +{ "success": false, "data": null, "errors": [{ "code": "ERR_XXX_000", "message": "..." }] } +``` + +--- + +## Modules + +| File | Domain | Endpoints | +|------|--------|-----------| +| [auth.md](auth.md) | Authentication — OTP, Login, JWT | 8 | +| [doctor.md](doctor.md) | Doctor profile & addresses | 11 | +| [clinic.md](clinic.md) | Clinics | 7 | +| [clinic-invitation.md](clinic-invitation.md) | Doctor invitations to clinics | 8 | +| [appointment.md](appointment.md) | Appointments & slot booking | 6 | +| [appointment-settings.md](appointment-settings.md) | Weekly schedule, date overrides, holidays | 14 | +| [payment.md](payment.md) | Payments (Mellat / Sep) | 5 | +| [settlement.md](settlement.md) | Wallet & settlement requests | 7 | +| [rating.md](rating.md) | Ratings, comments, likes | 9 | +| [secretary.md](secretary.md) | Doctor secretaries | 5 | +| [representation.md](representation.md) | Representations (agents) | 6 | +| [sms.md](sms.md) | SMS send & templates | 10 | +| [blog.md](blog.md) | Blog posts | 6 | +| [specialty.md](specialty.md) | Medical specialties | 5 | +| [insurance.md](insurance.md) | Insurances & doctor-insurance links | 10 | +| [doctor-service.md](doctor-service.md) | Doctor services | 5 | +| [tag.md](tag.md) | Blog tags | 5 | +| [location.md](location.md) | Provinces & cities | 10 | +| [user-profile.md](user-profile.md) | User medical profile | 4 | +| [admin.md](admin.md) | Admin dashboard & management | 25+ | + +--- + +## Error Code Reference + +| Code | Message (FA) | HTTP | +|------|--------------|------| +| `ERR_AUTH_001` | توکن JWT منقضی یا نامعتبر | 401 | +| `ERR_AUTH_002` | کد OTP نامعتبر | 401 | +| `ERR_AUTH_003` | کد OTP منقضی شده | 401 | +| `ERR_AUTH_004` | تعداد تلاش‌های OTP به حد مجاز رسیده | 429 | +| `ERR_AUTH_005` | نام کاربری یا رمز عبور اشتباه | 401 | +| `ERR_AUTH_006` | دسترسی ممنوع | 403 | +| `ERR_VALIDATION_001` | ورودی نامعتبر | 422 | +| `ERR_VALIDATION_002` | فیلد الزامی وارد نشده | 422 | +| `ERR_NOT_FOUND_001` | منبع درخواستی یافت نشد | 404 | +| `ERR_CONFLICT_001` | تداخل: منبع در حال استفاده | 409 | +| `ERR_FORBIDDEN_001` | دسترسی به این منبع مجاز نیست | 403 | +| `ERR_PAYMENT_001` | درگاه پرداخت در دسترس نیست | 503 | +| `ERR_PAYMENT_002` | مبلغ پرداخت نامعتبر | 422 | +| `ERR_PAYMENT_003` | وضعیت نوبت برای پرداخت مناسب نیست | 422 | +| `ERR_FILE_001` | فرمت فایل مجاز نیست | 422 | +| `ERR_SMS_003` | تمپلیت قبلاً ارسال شده | 422 | +| `ERR_SECRETARY_001` | پلن فعلی اجازه منشی بیشتر نمی‌دهد | 422 | +| `ERR_RATE_LIMIT_001` | درخواست‌های زیاد، بعداً تلاش کنید | 429 | diff --git a/docs/api/admin.md b/docs/api/admin.md new file mode 100644 index 00000000..0912d976 --- /dev/null +++ b/docs/api/admin.md @@ -0,0 +1,679 @@ +# Admin API + +> **Prefix:** `/api/v1/admin` +> **Permission:** ALL endpoints in this file require `ROLE_ADMIN` +> **Headers:** `Authorization: Bearer ` + +--- + +## Dashboard + +### GET `/api/v1/admin/dashboard/stats` + +Get key performance indicators (KPIs) for the dashboard. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ + "success": true, + "data": { + "total_users": 1200, + "active_doctors": 85, + "total_doctors": 92, + "total_clinics": 34, + "today_appointments": 47, + "total_appointments": 8540, + "today_payments_count": 30, + "today_payments_amount": 15000000, + "total_payments_amount": 425000000, + "pending_comments": 12, + "pending_settlements": 5, + "this_month_revenue": 52000000, + "this_month_appointments": 620 + } +} +``` + +--- + +### GET `/api/v1/admin/dashboard/charts` + +Get chart data for the last 30 days. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ + "success": true, + "data": { + "appointments_30d": [ + { "date": "2024-06-01", "count": 42 } + ], + "revenue_30d": [ + { "date": "2024-06-01", "amount_rials": 21000000 } + ], + "appointment_status": { + "confirmed": 350, + "completed": 180, + "cancelled": 45, + "pending": 20, + "no_show": 25 + }, + "top_specialties": [ + { "name": "قلب و عروق", "count": 120 } + ] + } +} +``` + +--- + +### GET `/api/v1/admin/dashboard/recent` + +Get recent activity (last 10 of each type). + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ + "success": true, + "data": { + "appointments": [ + { + "uuid": "...", + "doctor_title": "دکتر علی احمدی", + "patient_name": "محمد رضایی", + "slot_start": 1718438400, + "status": "confirmed" + } + ], + "payments": [ + { + "uuid": "...", + "amount_rials": 500000, + "gateway": "mellat", + "status": "paid", + "created_at": 1717000000 + } + ], + "users": [ + { + "uuid": "...", + "real_name": "محمد رضایی", + "mobile_number": "09...", + "roles": ["ROLE_USER"], + "created_at": 1717000000 + } + ] + } +} +``` + +--- + +## User Management + +### GET `/api/v1/admin/users` + +List all users with pagination and filters. + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search by name or mobile | +| `role` | string | ❌ | Filter: `ROLE_USER`, `ROLE_DOCTOR`, `ROLE_ADMIN`, etc. | +| `status` | string | ❌ | `"active"` or `"inactive"` | +| `sort` | string | ❌ | `"created_at"` (default desc) | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "real_name": "علی احمدی", + "mobile_number": "09123456789", + "roles": ["ROLE_USER"], + "status": "active", + "created_at": 1717000000 + } + ], + "meta": { "totalRecords": 1200, "totalPages": 60, "currentPage": 1 } +} +``` + +--- + +### GET `/api/v1/admin/users/{uuid}` + +Get detailed user info. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ + "success": true, + "data": { + "uuid": "...", + "real_name": "علی احمدی", + "mobile_number": "09123456789", + "roles": ["ROLE_USER"], + "status": "active", + "wallet_balance_rials": 500000, + "appointments_count": 5, + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | User not found | + +--- + +### GET `/api/v1/admin/users/stats` + +Get user statistics. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ + "success": true, + "data": { + "total": 1200, + "active": 1150, + "inactive": 50, + "admins": 3, + "doctors": 92, + "patients": 1100 + } +} +``` + +--- + +### PUT `/api/v1/admin/users/{uuid}` + +Update user info (name, email, password). + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "real_name": "علی احمدی جدید", + "password": "newPassword123" +} +``` + +### Response `200` +Updated user object. + +--- + +### PUT `/api/v1/admin/users/{uuid}/role` + +Change a user's role. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "role": "ROLE_DOCTOR" +} +``` + +| Field | Type | Required | Allowed Values | +|-------|------|----------|----------------| +| `role` | string | ✅ | `ROLE_USER`, `ROLE_DOCTOR`, `ROLE_CLINIC`, `ROLE_SECRETARY`, `ROLE_ADMIN` | + +### Response `200` +```json +{ "success": true, "data": { "message": "نقش کاربر تغییر کرد", "roles": ["ROLE_DOCTOR"] } } +``` + +--- + +### POST `/api/v1/admin/users/{uuid}/status` + +Toggle user active/inactive status. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "status": "inactive" } } +``` + +--- + +### DELETE `/api/v1/admin/users/{uuid}` + +Delete a user. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "کاربر حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | User not found | + +--- + +## Doctor Management + +### GET `/api/v1/admin/doctors` + +List all doctors with pagination. + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search in title | +| `status` | string | ❌ | `"active"` or `"inactive"` | +| `gender` | string | ❌ | `"male"` or `"female"` | +| `specialty_id` | integer | ❌ | Filter by specialty | +| `sort` | string | ❌ | Sort field | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "title": "دکتر علی احمدی", + "degree": "متخصص", + "gender": "male", + "doctor_rate": 4.5, + "active_doctor_appointment": true + } + ], + "meta": { "totalRecords": 92, "totalPages": 5, "currentPage": 1 } +} +``` + +--- + +### GET `/api/v1/admin/doctors/stats` + +Get doctor statistics. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ + "success": true, + "data": { + "total": 92, + "active": 85, + "inactive": 7, + "male": 60, + "female": 32, + "top_specialty": "قلب و عروق" + } +} +``` + +--- + +### POST `/api/v1/admin/doctors/{uuid}/status` + +Toggle doctor active status. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "active": false } } +``` + +--- + +## Clinic Management + +### GET `/api/v1/admin/clinics` + +List all clinics with pagination. + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search by clinic name | +| `status` | string | ❌ | `"active"` or `"inactive"` | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "name": "کلینیک الوند", + "city": "تهران", + "telephone": "02112345678", + "is_active": true, + "created_at": 1717000000 + } + ], + "meta": { "totalRecords": 34, "totalPages": 2, "currentPage": 1 } +} +``` + +--- + +### PATCH `/api/v1/admin/clinic/{uuid}/status` + +Toggle clinic active/inactive. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "is_active": false } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Clinic not found | + +--- + +### DELETE `/api/v1/admin/clinic/{uuid}` + +Delete a clinic. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "کلینیک حذف شد" } } +``` + +--- + +## Appointment Management + +### GET `/api/v1/admin/appointments` + +List all appointments. + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search by doctor/patient name | +| `status` | string | ❌ | Filter by status | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "doctor_title": "دکتر علی احمدی", + "patient_name": "محمد رضایی", + "slot_start": 1718438400, + "status": "confirmed", + "price": 500000 + } + ], + "meta": { "totalRecords": 8540, "totalPages": 427, "currentPage": 1 } +} +``` + +--- + +## Payment Management + +### GET `/api/v1/admin/payments` + +List all payments. + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `status` | string | ❌ | `"pending"`, `"paid"`, `"failed"`, `"cancelled"` | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "order_id": "CLINICPRO-...", + "amount_rials": 500000, + "status": "paid", + "gateway": "mellat", + "created_at": 1717000000 + } + ], + "meta": { "totalRecords": 7800, "totalPages": 390, "currentPage": 1 } +} +``` + +--- + +## Settlement Management + +### GET `/api/v1/admin/settlements` + +List all settlement requests. + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `status` | string | ❌ | `"pending"`, `"approved"`, `"rejected"` | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "user": { "uuid": "...", "real_name": "دکتر علی احمدی" }, + "amount_rials": 1000000, + "status": "pending", + "bank_account": { "bank_name": "بانک ملت", "owner_name": "..." }, + "created_at": 1717000000 + } + ], + "meta": { "totalRecords": 45, "totalPages": 3, "currentPage": 1 } +} +``` + +> To approve or reject, use the Settlement API: `POST /api/v1/settlement/{uuid}/approve` or `/reject` + +--- + +## Representation Management + +### GET `/api/v1/admin/representations` + +List all representations. + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search by name | +| `city_id` | integer | ❌ | Filter by city | + +### Response `200` +Paginated representation list. + +--- + +## Secretary Management + +### GET `/api/v1/admin/secretaries` + +List all secretaries. + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search by mobile | + +### Response `200` +Paginated secretary list with linked doctor info. + +--- + +## Rating & Comment Management + +### GET `/api/v1/admin/rates` + +List all ratings. + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search by doctor/patient | + +--- + +### GET `/api/v1/admin/comments` + +List all comments (all statuses). + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search in body | +| `status` | string | ❌ | `"pending"`, `"approved"`, `"rejected"` | + +> To approve/reject comments, use the Rating API: `POST /api/v1/admin/comment/{uuid}/approve` or `/reject` + +--- + +## SMS Management (Admin) + +### GET `/api/v1/admin/sms/logs` + +List SMS send logs. + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "id": 1, + "mobile": "09123456789", + "message": "کد تأیید: 123456", + "provider": "kavenegar", + "success": true, + "created_at": 1717000000 + } + ], + "meta": { "totalRecords": 5000, "totalPages": 250, "currentPage": 1 } +} +``` + +--- + +### GET `/api/v1/admin/sms/templates` + +List all SMS templates. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "name": "تأیید نوبت", + "status": "approved", + "provider_code": "verify_appointment", + "created_at": 1717000000 + } + ] +} +``` + +> To create/approve/reject templates, see [sms.md](sms.md) + +--- + +## Clinic Invitation Management + +> See [clinic-invitation.md](clinic-invitation.md) for full endpoint details. + +| Endpoint | Description | +|----------|-------------| +| `POST /api/v1/admin/clinic/{uuid}/invite-doctor` | Send invitation | +| `GET /api/v1/admin/clinic/{uuid}/invitations` | List invitations | +| `POST /api/v1/admin/clinic/invitation/{invUuid}/resend` | Resend SMS | +| `PATCH /api/v1/admin/clinic/invitation/{invUuid}/status` | Change status | +| `DELETE /api/v1/admin/clinic/invitation/{invUuid}` | Delete | diff --git a/docs/api/appointment-settings.md b/docs/api/appointment-settings.md new file mode 100644 index 00000000..92b53598 --- /dev/null +++ b/docs/api/appointment-settings.md @@ -0,0 +1,540 @@ +# Appointment Settings API + +> **Prefix:** `/api/v1/appointment-settings` +> **Permission:** All write endpoints require `AUTH` — must be the doctor owner or `ROLE_ADMIN` + +Doctors configure their availability via three resources: **weekly schedule**, **date overrides**, and **holidays**. + +--- + +## Weekly Schedule + +Each doctor has **one** weekly schedule (upsert). The schedule is keyed by **day index** (0=Saturday ... 6=Friday), each day containing a `sessions` array. + +### Day Index Convention + +| Index | Day (EN) | Day (FA) | +|-------|----------|----------| +| `"0"` | Saturday | شنبه | +| `"1"` | Sunday | یکشنبه | +| `"2"` | Monday | دوشنبه | +| `"3"` | Tuesday | سه‌شنبه | +| `"4"` | Wednesday | چهارشنبه | +| `"5"` | Thursday | پنجشنبه | +| `"6"` | Friday | جمعه | + +--- + +### POST `/api/v1/appointment-settings/weekly-schedule` + +Create or update the weekly schedule for a doctor (upsert). + +**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "doctor_uuid": "550e8400-e29b-41d4-a716-446655440000", + "schedule": { + "0": { + "sessions": [ + { + "active": true, + "location_id": 1973, + "start_time": "09:00", + "end_time": "13:00", + "duration_per_patient": 20, + "has_rest": true, + "rest_interval": 60, + "time_to_rest": 10, + "patient_limit": null + }, + { + "active": true, + "location_id": 1973, + "start_time": "15:00", + "end_time": "18:00", + "duration_per_patient": 20, + "has_rest": false, + "rest_interval": 0, + "time_to_rest": 0, + "patient_limit": 5 + } + ] + }, + "1": { "sessions": [] }, + "2": { "sessions": [] }, + "3": { "sessions": [] }, + "4": { "sessions": [] }, + "5": { "sessions": [] }, + "6": { "sessions": [] } + } +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID | +| `schedule` | object | ✅ | Keys `"0"` through `"6"` (day indices) | +| `schedule.{n}.sessions` | array | ✅ | Array of session config objects | + +**Session Config Object:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `active` | boolean | ✅ | Whether this session is active | +| `location_id` | integer\|null | ❌ | Doctor address/location ID | +| `start_time` | string | ✅ | Session start `"HH:MM"` | +| `end_time` | string | ✅ | Session end `"HH:MM"` | +| `duration_per_patient` | integer | ✅ | Minutes per appointment slot | +| `has_rest` | boolean | ❌ | Whether to insert rest breaks | +| `rest_interval` | integer | ❌ | Work minutes before taking a rest break | +| `time_to_rest` | integer | ❌ | Duration of each rest break (minutes) | +| `patient_limit` | integer\|null | ❌ | Max patients per session (`null` = unlimited) | + +> Multiple sessions per day are supported (e.g., morning + afternoon). Sessions are sorted by `start_time` and overlapping ones are skipped. + +### Response `201` +```json +{ + "success": true, + "data": { + "data": { + "uuid": "sched-uuid-...", + "doctor_uuid": "550e8400-...", + "schedule": { + "0": { "sessions": [ { "active": true, "start_time": "09:00", ... } ] }, + "1": { "sessions": [] }, + "2": { "sessions": [] }, + "3": { "sessions": [] }, + "4": { "sessions": [] }, + "5": { "sessions": [] }, + "6": { "sessions": [] } + }, + "created_at": 1717000000, + "updated_at": 1717000000 + } + } +} +``` + +> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data` +> This is an **upsert** — if a schedule already exists for the doctor, it is overwritten. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_VALIDATION_002` | 404 | Doctor not found | +| `ERR_AUTH_006` | 403 | Not the doctor owner | + +--- + +### GET `/api/v1/appointment-settings/weekly-schedule/{uuid}` + +Get weekly schedule. `{uuid}` can be either the **schedule UUID** or the **doctor UUID** — the controller tries both. + +**Permission:** `AUTH` (class-level `IS_AUTHENTICATED_FULLY`) + +### Response `200` +Same structure as POST response. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_VALIDATION_002` | 404 | Schedule not found | + +--- + +### PATCH `/api/v1/appointment-settings/weekly-schedule/{uuid}` + +Update weekly schedule. `{uuid}` can be schedule UUID or doctor UUID. + +**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN` + +### Request Body +```json +{ + "schedule": { + "0": { + "sessions": [ + { + "active": true, + "location_id": 1973, + "start_time": "10:00", + "end_time": "14:00", + "duration_per_patient": 30, + "has_rest": false, + "rest_interval": 0, + "time_to_rest": 0, + "patient_limit": null + } + ] + } + } +} +``` + +> Replaces the entire `schedule` object if provided. + +### Response `200` +Updated schedule object (same structure as POST). + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not the doctor owner | +| `ERR_VALIDATION_002` | 404 | Schedule not found | + +--- + +### DELETE `/api/v1/booking-setting/{uuid}` + +Delete a weekly schedule. + +**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN` + +> Note: route is `/booking-setting/`, not `/appointment-settings/` + +### Response `200` +```json +{ "success": true, "data": { "message": "برنامه هفتگی با موفقیت حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not the doctor owner | +| `ERR_VALIDATION_002` | 404 | Schedule not found | + +--- + +## Date Overrides + +Override a specific date — mark it inactive (day off) or give it custom sessions. + +### GET `/api/v1/appointment-settings/date-override/list/{doctorUuid}` + +Get all date overrides for a doctor. + +**Permission:** `AUTH` (class-level) + +### Response `200` +```json +{ + "success": true, + "data": { + "data": [ + { + "uuid": "...", + "doctor_uuid": "...", + "date": 1718476800, + "active": false, + "reason": "تعطیل خاص", + "custom_slots": [], + "created_at": 1717000000 + } + ] + } +} +``` + +> `date` is a Unix timestamp. `active: false` = entire day blocked. `active: true` with `custom_slots` = custom session schedule. + +--- + +### POST `/api/v1/appointment-settings/date-override` + +Create a date override. + +**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN` + +### Request Body +```json +{ + "doctor_uuid": "550e8400-...", + "date": "2024-06-20", + "active": false, + "reason": "تعطیل رسمی", + "custom_slots": null +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID | +| `date` | string | ✅ | Date in `Y-m-d` format (e.g. `"2024-06-20"`) | +| `active` | boolean | ❌ | `false` = full day off (default); `true` = use custom_slots | +| `reason` | string | ❌ | Reason for override | +| `custom_slots` | array\|null | ❌ | Custom sessions (same SessionConfig format as weekly schedule, see below) | + +**`custom_slots` format when `active: true`:** +```json +{ + "custom_slots": [ + { + "start_time": "14:00", + "end_time": "18:00", + "duration_per_patient": 20, + "location_id": 1973, + "has_rest": false, + "rest_interval": 0, + "time_to_rest": 0, + "patient_limit": null + } + ] +} +``` + +**Legacy format (backward compatible):** +```json +{ + "custom_slots": [ + { "start": "14:00", "end": "18:00", "duration": 20 } + ] +} +``` + +### Response `201` +```json +{ + "success": true, + "data": { + "data": { + "uuid": "override-uuid-...", + "doctor_uuid": "...", + "date": 1718476800, + "active": false, + "reason": "تعطیل رسمی", + "custom_slots": [], + "created_at": 1717000000 + } + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not the doctor owner | +| `ERR_VALIDATION_002` | 404 | Doctor not found | +| `ERR_VALIDATION_001` | 422 | Invalid date format | + +--- + +### GET `/api/v1/appointment-settings/date-override/{uuid}` + +Get a single date override. + +**Permission:** `AUTH` (class-level) + +### Response `200` +Override object (same structure as above). + +--- + +### PATCH `/api/v1/appointment-settings/date-override/{uuid}` + +Update a date override. + +**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN` + +### Request Body (all optional) +```json +{ + "active": true, + "reason": "جبران مرخصی", + "custom_slots": [ + { + "start_time": "14:00", + "end_time": "18:00", + "duration_per_patient": 20, + "location_id": 1973, + "has_rest": false, + "rest_interval": 0, + "time_to_rest": 0, + "patient_limit": null + } + ], + "date": "2024-06-21" +} +``` + +### Response `200` +Updated override object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not the doctor owner | +| `ERR_VALIDATION_002` | 404 | Override not found | + +--- + +### DELETE `/api/v1/appointment-settings/date-override/{uuid}` + +Delete a date override. + +**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "Override با موفقیت حذف شد" } } +``` + +--- + +## Holidays + +Mark a date range as holiday — all slots blocked, no overrides apply. + +### GET `/api/v1/appointment-settings/holidays/list/{doctorUuid}` + +Get all holidays for a doctor. + +**Permission:** `AUTH` (class-level) + +### Response `200` +```json +{ + "success": true, + "data": { + "data": [ + { + "uuid": "...", + "doctor_uuid": "...", + "start_date": 1719792000, + "end_date": 1720656000, + "reason": "تعطیلات تابستانی", + "active": true, + "created_at": 1717000000 + } + ] + } +} +``` + +> `start_date` and `end_date` are Unix timestamps. Holidays take **highest priority** — they block the day even if a date override exists. + +--- + +### POST `/api/v1/appointment-settings/holidays` + +Create a holiday range. + +**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN` + +### Request Body +```json +{ + "doctor_uuid": "550e8400-...", + "start_date": "2024-07-01", + "end_date": "2024-07-10", + "reason": "تعطیلات تابستانی" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID | +| `start_date` | string | ✅ | Start date `Y-m-d` | +| `end_date` | string | ✅ | End date `Y-m-d` (must be ≥ start_date) | +| `reason` | string | ❌ | Holiday reason | + +### Response `201` +```json +{ + "success": true, + "data": { + "data": { + "uuid": "holiday-uuid-...", + "doctor_uuid": "...", + "start_date": 1719792000, + "end_date": 1720656000, + "reason": "تعطیلات تابستانی", + "active": true, + "created_at": 1717000000 + } + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not the doctor owner | +| `ERR_VALIDATION_002` | 404 | Doctor not found | +| `ERR_VALIDATION_001` | 422 | end_date before start_date or invalid format | + +--- + +### GET `/api/v1/appointment-settings/holidays/{uuid}` + +Get a single holiday. + +**Permission:** `AUTH` (class-level) + +--- + +### PATCH `/api/v1/appointment-settings/holidays/{uuid}` + +Update a holiday. + +**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN` + +### Request Body (all optional) +```json +{ + "start_date": "2024-07-02", + "end_date": "2024-07-12", + "reason": "تمدید تعطیلات", + "active": false +} +``` + +### Response `200` +Updated holiday object. + +--- + +### DELETE `/api/v1/appointment-settings/holidays/{uuid}` + +Delete a holiday. + +**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "تعطیلات حذف شد" } } +``` + +--- + +## Slot Calculation Logic (Reference) + +The `SlotCalculatorService` calculates available slots in this priority order: + +1. **Holiday** — if date falls in a holiday range → return empty (no slots) +2. **Date Override** — if a date override exists for this date: + - `active: false` → return empty + - `active: true` → use `custom_slots` sessions +3. **Weekly Schedule** — use the day's `sessions` array (only `active: true` sessions, sorted by `start_time`, overlapping sessions skipped) + +**Slot output format:** +```json +[ + { + "start": 1718438400, + "end": 1718439600, + "start_time": "09:00", + "end_time": "09:20", + "location_id": 1973 + } +] +``` diff --git a/docs/api/appointment.md b/docs/api/appointment.md new file mode 100644 index 00000000..3e08a625 --- /dev/null +++ b/docs/api/appointment.md @@ -0,0 +1,277 @@ +# Appointment API + +> **Prefix:** `/api/v1/appointment*` + +--- + +## GET `/api/v1/appointment-slots` + +Get available appointment slots for a doctor on a specific date. + +**Permission:** `PUBLIC` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID | +| `date` | string | ✅ | Date in `Y-m-d` format (e.g. `2024-06-15`) | + +### Response `200` +```json +{ + "success": true, + "data": { + "doctor_uuid": "550e8400-...", + "date": "2024-06-15", + "slots": [ + { + "start": 1718438400, + "end": 1718439600, + "available": true + }, + { + "start": 1718439600, + "end": 1718440800, + "available": false + } + ] + } +} +``` + +> All times are **Unix timestamps**. Slots are calculated from `WeeklySchedule` minus booked appointments, date overrides, and holidays. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | +| `ERR_VALIDATION_001` | 422 | Missing or invalid date/doctor_uuid | + +--- + +## POST `/api/v1/appointment` + +Book an appointment slot. + +**Permission:** `AUTH` — any authenticated user + +### Request Body (`application/json`) +```json +{ + "doctor_uuid": "550e8400-e29b-41d4-a716-446655440000", + "slot_start": 1718438400, + "slot_end": 1718439600, + "note": "لطفاً سریع ویزیت شوم" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID | +| `slot_start` | integer | ✅ | Slot start (Unix timestamp) | +| `slot_end` | integer | ✅ | Slot end (Unix timestamp) | +| `note` | string | ❌ | Patient note | + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "appt-uuid-...", + "doctor": { "uuid": "...", "title": "دکتر علی احمدی" }, + "user": { "uuid": "...", "real_name": "..." }, + "slot_start": 1718438400, + "slot_end": 1718439600, + "status": "pending", + "note": "...", + "price": 500000, + "created_at": 1717000000 + } +} +``` + +**Appointment Status Values:** +| Value | Description | +|-------|-------------| +| `pending` | Awaiting payment | +| `confirmed` | Paid and confirmed | +| `cancelled` | Cancelled | +| `completed` | Visit completed | +| `no_show` | Patient did not show | + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | +| `ERR_CONFLICT_001` | 409 | Slot already booked | +| `ERR_VALIDATION_001` | 422 | Invalid slot times | + +--- + +## GET `/api/v1/appointment/{uuid}` + +Get appointment detail. + +**Permission:** `AUTH` — must be the patient, the doctor, or `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Appointment UUID | + +### Response `200` +```json +{ + "success": true, + "data": { + "uuid": "appt-uuid-...", + "doctor": { + "uuid": "...", + "title": "دکتر علی احمدی", + "image": "https://..." + }, + "user": { + "uuid": "...", + "real_name": "کاربر", + "mobile_number": "09..." + }, + "slot_start": 1718438400, + "slot_end": 1718439600, + "status": "confirmed", + "note": "...", + "price": 500000, + "payment_uuid": "pay-uuid-...", + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the patient/doctor/admin | +| `ERR_NOT_FOUND_001` | 404 | Appointment not found | + +--- + +## GET `/api/v1/appointments/doctor/{doctorUuid}` + +Get all appointments for a specific doctor. + +**Permission:** `AUTH` — must be the doctor, their secretary, or `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `doctorUuid` | string (UUID) | Doctor UUID | + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `status` | string | ❌ | Filter: `pending`, `confirmed`, `cancelled`, `completed`, `no_show` | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "user": { "uuid": "...", "real_name": "..." }, + "slot_start": 1718438400, + "slot_end": 1718439600, + "status": "confirmed", + "price": 500000 + } + ] +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not authorized to view this doctor's appointments | +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | + +--- + +## GET `/api/v1/appointments/user` + +Get all appointments for the authenticated user. + +**Permission:** `AUTH` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `status` | string | ❌ | Filter by status | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "doctor": { "uuid": "...", "title": "دکتر علی احمدی" }, + "slot_start": 1718438400, + "slot_end": 1718439600, + "status": "confirmed", + "price": 500000 + } + ] +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | + +--- + +## PATCH `/api/v1/appointment/{uuid}/status` + +Change appointment status. + +**Permission:** `AUTH` — patient can cancel; doctor/secretary can confirm/complete/no_show; admin can do all + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Appointment UUID | + +### Request Body +```json +{ + "status": "cancelled", + "version": 3 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `status` | string | ✅ | New status value | +| `version` | integer | ❌ | Optimistic lock version (prevents double-submit) | + +**Allowed Transitions by Role:** +| Actor | Allowed transitions | +|-------|---------------------| +| Patient | `pending → cancelled` | +| Doctor / Secretary | `pending → confirmed`, `confirmed → completed`, `confirmed → no_show` | +| Admin | Any transition | + +### Response `200` +Updated appointment object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not authorized for this transition | +| `ERR_NOT_FOUND_001` | 404 | Appointment not found | +| `ERR_CONFLICT_001` | 409 | Version mismatch (optimistic lock) | +| `ERR_VALIDATION_001` | 422 | Invalid status value | diff --git a/docs/api/auth.md b/docs/api/auth.md new file mode 100644 index 00000000..163255f2 --- /dev/null +++ b/docs/api/auth.md @@ -0,0 +1,284 @@ +# Authentication API + +> **Prefix:** `/api/v1/user` and `/oauth` +> **Permission:** All endpoints in this module are **PUBLIC** (no JWT required) except `userinfo` and `logout` + +--- + +## POST `/api/v1/user/send-code` + +Send OTP code to mobile number. + +**Permission:** `PUBLIC` + +### Request Body +```json +{ + "mobile": "09123456789" +} +``` + +| Field | Type | Required | Validation | +|-------|------|----------|------------| +| `mobile` | string | ✅ | Format: `09XXXXXXXXX` (11 digits) | + +### Response `200` +```json +{ + "success": true, + "data": { + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "message": "کد تأیید ارسال شد" + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_VALIDATION_001` | 422 | Invalid mobile format | +| `ERR_AUTH_004` | 429 | OTP rate limit exceeded | + +--- + +## POST `/api/v1/user/verify-code` + +Verify OTP code. Returns whether this is a new or existing user. + +**Permission:** `PUBLIC` + +### Request Body +```json +{ + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "code": "123456" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `uuid` | string | ✅ | UUID returned from `send-code` | +| `code` | string | ✅ | 6-digit OTP | + +### Response `200` +```json +{ + "success": true, + "data": { + "message": "کد تأیید شد", + "is_new_user": false, + "uuid": "550e8400-e29b-41d4-a716-446655440000" + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_002` | 401 | Invalid OTP code | +| `ERR_AUTH_003` | 401 | OTP expired | +| `ERR_VALIDATION_002` | 422 | Missing required field | + +--- + +## POST `/api/v1/user/register` + +Complete registration for new users (called only when `is_new_user: true`). + +**Permission:** `PUBLIC` + +### Request Body +```json +{ + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "real_name": "علی احمدی" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `uuid` | string | ✅ | Verified UUID from `verify-code` | +| `real_name` | string | ❌ | User's full name | + +### Response `201` +```json +{ + "success": true, + "data": { + "message": "ثبت‌نام با موفقیت انجام شد", + "uuid": "550e8400-e29b-41d4-a716-446655440000" + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_VALIDATION_002` | 422 | Missing uuid | +| `ERR_CONFLICT_001` | 409 | User already registered | + +--- + +## POST `/api/v1/user/login` + +Login with mobile number and password (for users who set a password). + +**Permission:** `PUBLIC` + +### Request Body +```json +{ + "mobile_number": "09123456789", + "password": "mypassword" +} +``` + +| Field | Type | Required | +|-------|------|----------| +| `mobile_number` | string | ✅ | +| `password` | string | ✅ | + +### Response `200` +```json +{ + "success": true, + "data": { + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", + "refresh_token": "def50200..." + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_005` | 401 | Wrong credentials | +| `ERR_AUTH_006` | 403 | Account suspended | +| `ERR_AUTH_004` | 429 | Too many attempts | + +--- + +## POST `/oauth/token` + +Exchange verified UUID for JWT access token. + +**Permission:** `PUBLIC` + +### Request Body +```json +{ + "grant_type": "mobile", + "uuid": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `grant_type` | string | ✅ | Must be `"mobile"` | +| `uuid` | string | ✅ | UUID from verified OTP flow | + +### Response `200` +```json +{ + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", + "refresh_token": "def50200..." +} +``` + +> JWT payload: `{ username: mobile_number, roles: [...], iat, exp }` +> Access token TTL: **1 hour** | Refresh token TTL: **30 days** (stored in Redis) + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_002` | 400 | Invalid or expired UUID | + +--- + +## POST `/oauth/token/refresh` + +Refresh expired JWT using refresh token. + +**Permission:** `PUBLIC` + +### Request Body +```json +{ + "refresh_token": "def50200..." +} +``` + +### Response `200` +```json +{ + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", + "refresh_token": "def50200..." +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Invalid or expired refresh token | + +--- + +## GET `/oauth/userinfo` + +Get authenticated user info. + +**Permission:** `AUTH` — requires valid JWT + +### Headers +``` +Authorization: Bearer +``` + +### Response `200` +```json +{ + "success": true, + "data": { + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "mobile_number": "09123456789", + "real_name": "علی احمدی", + "roles": ["ROLE_USER"], + "status": "active", + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Invalid or missing token | + +--- + +## POST `/oauth/logout` + +Invalidate the refresh token (stored in Redis). + +**Permission:** `AUTH` + +### Request Body +```json +{ + "refresh_token": "def50200..." +} +``` + +| Field | Type | Required | +|-------|------|----------| +| `refresh_token` | string | ❌ | + +### Response `200` +```json +{ + "success": true, + "data": { + "message": "با موفقیت خارج شدید" + } +} +``` diff --git a/docs/api/blog.md b/docs/api/blog.md new file mode 100644 index 00000000..b1d531cd --- /dev/null +++ b/docs/api/blog.md @@ -0,0 +1,232 @@ +# Blog API + +> **Prefix:** `/api/v1/blog`, `/api/v1/blogs` + +--- + +## GET `/api/v1/blogs` + +List published blog posts. + +**Permission:** `PUBLIC` + +### Query Parameters +| Param | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `page` | integer | ❌ | 1 | Page number | +| `limit` | integer | ❌ | 20 | Items per page | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "title": "آشنایی با بیماری دیابت", + "slug": "ashnayi-ba-bimari-diabat", + "summary": "خلاصه مطلب...", + "image": "https://...", + "author": { "uuid": "...", "real_name": "دکتر احمدی" }, + "tags": [{ "id": 1, "name": "دیابت" }], + "status": "published", + "created_at": 1717000000 + } + ], + "meta": { + "totalRecords": 25, + "totalPages": 2, + "currentPage": 1 + } +} +``` + +--- + +## GET `/api/v1/blog/{slug}` + +Get a single blog post by slug. + +**Permission:** `PUBLIC` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `slug` | string | URL slug (e.g. `ashnayi-ba-bimari-diabat`) | + +### Response `200` +```json +{ + "success": true, + "data": { + "uuid": "...", + "title": "آشنایی با بیماری دیابت", + "slug": "ashnayi-ba-bimari-diabat", + "summary": "خلاصه...", + "body": "

محتوای کامل...

", + "image": "https://...", + "author": { "uuid": "...", "real_name": "دکتر احمدی" }, + "tags": [{ "id": 1, "name": "دیابت" }], + "status": "published", + "created_at": 1717000000, + "updated_at": 1717100000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Blog not found or not published | + +--- + +## POST `/api/v1/blog` + +Create a new blog post. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "title": "آشنایی با بیماری دیابت", + "body": "

محتوای کامل مقاله...

", + "summary": "خلاصه کوتاه از مقاله", + "tags": [1, 2], + "status": "draft" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `title` | string | ✅ | Post title (slug auto-generated) | +| `body` | string | ✅ | Full HTML body | +| `summary` | string | ❌ | Short excerpt | +| `tags` | integer[] | ❌ | Array of tag IDs | +| `status` | string | ❌ | `"draft"` (default) or `"published"` | + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "blog-uuid-...", + "title": "آشنایی با بیماری دیابت", + "slug": "ashnayi-ba-bimari-diabat", + "summary": "...", + "body": "...", + "image": null, + "tags": [], + "status": "draft", + "created_at": 1717000000 + } +} +``` + +**Blog Status Values:** +| Value | Description | +|-------|-------------| +| `draft` | Not visible to public | +| `published` | Visible in public listing | +| `archived` | Hidden from listing | + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_VALIDATION_002` | 422 | Missing required fields | + +--- + +## PATCH `/api/v1/blog/{uuid}` + +Update a blog post. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Blog UUID | + +### Request Body (`application/json`) +```json +{ + "title": "عنوان جدید", + "body": "

محتوای جدید

", + "summary": "خلاصه جدید", + "tags": [1, 3], + "status": "published" +} +``` + +All fields optional. + +### Response `200` +Updated blog object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Blog not found | + +--- + +## DELETE `/api/v1/blog/{uuid}` + +Delete a blog post. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "مقاله حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Blog not found | + +--- + +## POST `/file/upload/clinic_pro/blog/field_image` + +Upload blog post header image. + +**Permission:** `ROLE_ADMIN` + +### Request +`Content-Type: multipart/form-data` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file` | binary | ✅ | Image (max 5MB) | +| `blog_uuid` | string (UUID) | ❌ | Auto-associate with blog post | + +### Response `200` +```json +{ + "success": true, + "data": { + "url": "https://clinic-pro.ddev.site/uploads/blog/post_abc.jpg", + "uuid": "...", + "filename": "post_abc.jpg", + "filemime": "image/jpeg", + "filesize": 204800 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_FILE_001` | 422 | Invalid file type | diff --git a/docs/api/clinic-invitation.md b/docs/api/clinic-invitation.md new file mode 100644 index 00000000..00f1b7c9 --- /dev/null +++ b/docs/api/clinic-invitation.md @@ -0,0 +1,312 @@ +# Clinic Doctor Invitation API + +> **Prefix:** `/api/v1/admin/clinic/...` (admin) and `/api/v1/clinic-invitation/...` (public) + +Admins invite doctors to clinics via SMS. The doctor receives a secure 96-char token link valid for 72 hours. + +--- + +## POST `/api/v1/admin/clinic/{uuid}/invite-doctor` + +Send an invitation to a doctor (by mobile number) to join a clinic. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Clinic UUID | + +### Request Body (`application/json`) +```json +{ + "mobile": "09123456789", + "name": "دکتر علی احمدی", + "specialty": "قلب و عروق" +} +``` + +| Field | Type | Required | Validation | +|-------|------|----------|------------| +| `mobile` | string | ✅ | Format: `09XXXXXXXXX` | +| `name` | string | ❌ | Doctor's display name | +| `specialty` | string | ❌ | Specialty label for SMS | + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "inv-uuid-...", + "mobile": "09123456789", + "invited_name": "دکتر علی احمدی", + "invited_specialty": "قلب و عروق", + "status": "pending", + "invited_at": 1717000000, + "expires_at": 1717259200, + "token_used": false, + "clinic": { "uuid": "...", "name": "کلینیک الوند" } + } +} +``` + +> SMS is dispatched **asynchronously** via Symfony Messenger → Redis queue. +> SMS text: `"دکتر گرامی، کلینیک {name} شما را برای همکاری دعوت کرده است.\nبرای بررسی: {link}\nاین لینک تا ۷۲ ساعت معتبر است."` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing or invalid token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Clinic not found | +| `ERR_VALIDATION_001` | 422 | Invalid mobile format | + +--- + +## GET `/api/v1/admin/clinic/{uuid}/invitations` + +List all invitations for a clinic. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Clinic UUID | + +### Query Parameters +| Param | Type | Required | Default | +|-------|------|----------|---------| +| `page` | integer | ❌ | 1 | +| `limit` | integer | ❌ | 50 | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "mobile": "09123456789", + "invited_name": "دکتر علی احمدی", + "invited_specialty": "قلب و عروق", + "status": "pending", + "invited_at": 1717000000, + "expires_at": 1717259200, + "responded_at": null, + "token_used": false, + "doctor": null + } + ], + "meta": { + "totalRecords": 5, + "totalPages": 1, + "currentPage": 1 + } +} +``` + +**Invitation Status Values:** +| Value | Description | +|-------|-------------| +| `pending` | Sent, awaiting response | +| `accepted` | Doctor accepted | +| `rejected` | Doctor rejected | +| `suspended` | Suspended by admin | +| `removed` | Removed | + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Clinic not found | + +--- + +## POST `/api/v1/admin/clinic/invitation/{invUuid}/resend` + +Resend the invitation SMS with a fresh token and reset expiry to +72 hours. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `invUuid` | string (UUID) | Invitation UUID | + +### Response `200` +```json +{ + "success": true, + "data": { "message": "دعوتنامه مجدداً ارسال شد" } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Invitation not found | + +--- + +## PATCH `/api/v1/admin/clinic/invitation/{invUuid}/status` + +Change the status of an invitation (e.g., suspend or remove). + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `invUuid` | string (UUID) | Invitation UUID | + +### Request Body +```json +{ + "status": "suspended" +} +``` + +| Field | Type | Required | Allowed Values | +|-------|------|----------|----------------| +| `status` | string | ✅ | `pending`, `suspended`, `removed` | + +### Response `200` +```json +{ + "success": true, + "data": { "status": "suspended" } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Invitation not found | +| `ERR_VALIDATION_001` | 422 | Invalid status value | + +--- + +## DELETE `/api/v1/admin/clinic/invitation/{invUuid}` + +Delete an invitation. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `invUuid` | string (UUID) | Invitation UUID | + +### Response `204` +Empty body. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Invitation not found | + +--- + +## GET `/api/v1/clinic-invitation/{token}` + +View invitation details by token (used on the doctor-facing landing page). + +**Permission:** `PUBLIC` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `token` | string | 96-char hex token from SMS link | + +### Response `200` +```json +{ + "success": true, + "data": { + "invitation": { + "uuid": "...", + "mobile": "09123456789", + "invited_name": "دکتر علی احمدی", + "invited_specialty": "قلب و عروق", + "status": "pending", + "expires_at": 1717259200 + }, + "clinic": { + "uuid": "...", + "name": "کلینیک الوند", + "city": "تهران", + "clinic_logo": "https://..." + }, + "is_usable": true + } +} +``` + +> `is_usable: false` when: token already used, expired, or status is not `pending` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Token not found | + +--- + +## POST `/api/v1/clinic-invitation/{token}/accept` + +Doctor accepts the invitation. If a doctor profile exists for this mobile, they are automatically linked to the clinic. + +**Permission:** `PUBLIC` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `token` | string | 96-char hex token | + +### Response `200` +```json +{ + "success": true, + "data": { "message": "دعوتنامه پذیرفته شد" } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Token not found | +| `ERR_VALIDATION_001` | 422 | Token expired or already used | + +--- + +## POST `/api/v1/clinic-invitation/{token}/reject` + +Doctor rejects the invitation. + +**Permission:** `PUBLIC` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `token` | string | 96-char hex token | + +### Response `200` +```json +{ + "success": true, + "data": { "message": "دعوتنامه رد شد" } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Token not found | +| `ERR_VALIDATION_001` | 422 | Token expired or already used | diff --git a/docs/api/clinic.md b/docs/api/clinic.md new file mode 100644 index 00000000..50cf4c48 --- /dev/null +++ b/docs/api/clinic.md @@ -0,0 +1,310 @@ +# Clinic API + +> **Prefix:** `/api/v1/clinic`, `/api/v1/clinics` + +--- + +## POST `/api/v1/clinic` + +Create a new clinic. + +**Permission:** `AUTH` — any authenticated user becomes the clinic owner + +### Request Body (`application/json`) +```json +{ + "name": "کلینیک الوند", + "info": "توضیحات کلینیک", + "address": "تهران، خیابان ولیعصر", + "telephone": "02112345678", + "working_days": "شنبه تا چهارشنبه", + "is_24_7": false, + "latitude": 35.6892, + "longitude": 51.3890, + "state": "تهران", + "city": "تهران", + "image_clinic": [ + { "url": "https://..." } + ], + "clinic_logo": "https://...", + "doctors": ["uuid1", "uuid2"], + "specialties": [1, 2], + "doctor_services": [3, 4], + "insurance": [5, 6] +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | ✅ | Clinic name | +| `info` | string | ❌ | Description | +| `address` | string | ❌ | Full address | +| `telephone` | string | ❌ | Contact number | +| `working_days` | string | ❌ | Working days description | +| `is_24_7` | boolean | ❌ | Open 24/7 flag | +| `latitude` | float | ❌ | Latitude for map | +| `longitude` | float | ❌ | Longitude for map | +| `state` | string | ❌ | Province name | +| `city` | string | ❌ | City name | +| `image_clinic` | object[] | ❌ | Gallery images `[{url: "..."}]` | +| `clinic_logo` | string | ❌ | Logo URL | +| `doctors` | string[] | ❌ | Doctor UUIDs to associate | +| `specialties` | integer[] | ❌ | Specialty IDs | +| `doctor_services` | integer[] | ❌ | Service IDs | +| `insurance` | integer[] | ❌ | Insurance IDs | + +### Response `201` +```json +{ + "success": true, + "data": { + "data": { + "uuid": "550e8400-...", + "name": "کلینیک الوند", + "info": "...", + "address": "...", + "telephone": "02112345678", + "working_days": "...", + "is_24_7": false, + "latitude": 35.6892, + "longitude": 51.3890, + "state": "تهران", + "city": "تهران", + "images_clinic": [{ "url": "https://..." }], + "clinic_logo": "https://...", + "is_active": true, + "doctors": [], + "specialties": [], + "doctor_services": [], + "insurance": [], + "tags": [], + "created_at": 1717000000 + } + } +} +``` + +> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing or invalid token | + +--- + +## GET `/api/v1/clinic/{uuid}` + +Get clinic detail. + +**Permission:** `PUBLIC` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Clinic UUID | + +### Response `200` +```json +{ + "success": true, + "data": { + "data": { + "uuid": "550e8400-...", + "name": "کلینیک الوند", + "info": "...", + "address": "تهران، خیابان ولیعصر", + "telephone": "02112345678", + "working_days": "شنبه تا چهارشنبه", + "is_24_7": false, + "latitude": 35.6892, + "longitude": 51.3890, + "state": "تهران", + "city": "تهران", + "images_clinic": [{ "url": "https://..." }], + "clinic_logo": "https://...", + "is_active": true, + "doctors": [{ "uuid": "...", "title": "دکتر..." }], + "specialties": [{ "id": 1, "name": "قلب" }], + "doctor_services": [], + "insurance": [], + "tags": [], + "owner": { "uuid": "...", "real_name": "..." } + } + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Clinic not found | + +--- + +## PATCH `/api/v1/clinic/{uuid}` + +Update a clinic. + +**Permission:** `AUTH` — must be the clinic owner or `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Clinic UUID | + +### Request Body +Same fields as POST — all optional. + +### Response `200` +Updated clinic object (same structure as GET). + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the owner | +| `ERR_NOT_FOUND_001` | 404 | Clinic not found | + +--- + +## GET `/api/v1/clinics` + +List clinics with pagination. + +**Permission:** `PUBLIC` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `name` | string | ❌ | Search by clinic name | +| `city` | string | ❌ | Filter by city | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "name": "کلینیک الوند", + "city": "تهران", + "clinic_logo": "https://...", + "is_active": true + } + ], + "meta": { + "totalRecords": 30, + "totalPages": 2, + "currentPage": 1 + } +} +``` + +--- + +## GET `/api/v1/clinic/doctor-list/{clinicUuid}` + +Get doctors associated with a clinic. + +**Permission:** `PUBLIC` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `clinicUuid` | string (UUID) | Clinic UUID | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "title": "دکتر علی احمدی", + "degree": "متخصص", + "doctor_rate": 4.5, + "image": "https://..." + } + ] +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Clinic not found | + +--- + +## POST `/file/upload/clinic_pro/clinic/field_clinic_logo` + +Upload clinic logo. + +**Permission:** `AUTH` + +### Request +`Content-Type: multipart/form-data` + +| Field | Type | Required | Max Size | +|-------|------|----------|----------| +| `file` | binary | ✅ | 5MB | + +### Response `200` +```json +{ + "success": true, + "data": { + "url": "https://clinic-pro.ddev.site/uploads/clinic/logo_abc.jpg", + "uuid": "...", + "filename": "logo_abc.jpg", + "filemime": "image/jpeg", + "filesize": 102400 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_FILE_001` | 422 | Invalid file type | +| `ERR_AUTH_001` | 401 | Missing token | + +--- + +## POST `/file/upload/clinic_pro/clinic/field_image_clinic` + +Upload clinic gallery image. + +**Permission:** `AUTH` + +### Request +`Content-Type: multipart/form-data` + +| Field | Type | Required | Max Size | +|-------|------|----------|----------| +| `file` | binary | ✅ | 5MB | + +### Response `200` +```json +{ + "success": true, + "data": { + "url": "https://clinic-pro.ddev.site/uploads/clinic/gallery_abc.jpg", + "uuid": "...", + "filename": "gallery_abc.jpg", + "filemime": "image/jpeg", + "filesize": 307200 + } +} +``` + +> After uploading, use the returned `url` inside `image_clinic: [{ "url": "..." }]` when calling PATCH clinic. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_FILE_001` | 422 | Invalid file type | +| `ERR_AUTH_001` | 401 | Missing token | diff --git a/docs/api/doctor-service.md b/docs/api/doctor-service.md new file mode 100644 index 00000000..e1488142 --- /dev/null +++ b/docs/api/doctor-service.md @@ -0,0 +1,146 @@ +# Doctor Service API + +> **Prefix:** `/api/v1/doctor-services`, `/api/v1/admin/doctor-service` + +--- + +## GET `/api/v1/doctor-services` + +List all active doctor services. + +**Permission:** `PUBLIC` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `specialty_id` | integer | ❌ | Filter by parent specialty | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "id": 1, + "name": "نوار قلب (ECG)", + "slug": "navar-ghalb", + "specialty_id": 1, + "status": "active", + "weight": 5 + } + ] +} +``` + +--- + +## GET `/api/v1/admin/doctor-services` + +List all services with pagination (admin view). + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search in name | +| `specialty_id` | integer | ❌ | Filter by specialty | + +### Response `200` +```json +{ + "success": true, + "data": [ ... ], + "meta": { "totalRecords": 30, "totalPages": 2, "currentPage": 1 } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | + +--- + +## POST `/api/v1/admin/doctor-service` + +Create a new doctor service. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "name": "نوار قلب (ECG)", + "slug": "navar-ghalb", + "specialty_id": 1, + "status": "active", + "weight": 5 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | ✅ | Service name | +| `slug` | string | ❌ | Auto-generated from name if omitted | +| `specialty_id` | integer | ❌ | Link to specialty | +| `status` | string | ❌ | `"active"` or `"inactive"` | +| `weight` | integer | ❌ | Sort weight | + +### Response `201` +Service object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_VALIDATION_002` | 422 | Missing name | + +--- + +## PATCH `/api/v1/admin/doctor-service/{id}` + +Update a doctor service. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `id` | integer | Service ID | + +All body fields optional. + +### Response `200` +Updated service object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Service not found | + +--- + +## DELETE `/api/v1/admin/doctor-service/{id}` + +Delete a doctor service. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "خدمت پزشکی حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Service not found | diff --git a/docs/api/doctor.md b/docs/api/doctor.md new file mode 100644 index 00000000..d70a3e17 --- /dev/null +++ b/docs/api/doctor.md @@ -0,0 +1,380 @@ +# Doctor API + +> **Prefix:** `/api/v1/doctor`, `/api/v1/doctors`, `/api/v1/clinic-pro/doctor-address*` + +--- + +## POST `/api/v1/doctor` + +Create a doctor profile for the authenticated user. + +**Permission:** `AUTH` — any authenticated user + +### Request Body (`application/json`) +```json +{ + "title": "دکتر علی احمدی", + "gender": "male", + "medical_system_code": "12345", + "degree": "متخصص", + "info": "توضیحات درباره پزشک", + "specialties": [1, 2], + "doctor_services": [3, 4] +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `title` | string | ✅ | Full name with title | +| `gender` | string | ❌ | `"male"` or `"female"` | +| `medical_system_code` | string | ❌ | Nظام پزشکی code | +| `degree` | string | ❌ | Academic degree | +| `info` | string | ❌ | Bio/description | +| `specialties` | integer[] | ❌ | Array of specialty IDs | +| `doctor_services` | integer[] | ❌ | Array of doctor service IDs | + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "title": "دکتر علی احمدی", + "gender": "male", + "medical_system_code": "12345", + "degree": "متخصص", + "info": "...", + "doctor_rate": null, + "active_doctor_appointment": false, + "specialties": [], + "doctor_services": [], + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing or invalid token | +| `ERR_CONFLICT_001` | 409 | Doctor profile already exists for this user | +| `ERR_VALIDATION_002` | 422 | Missing required field | + +--- + +## GET `/api/v1/doctor/{uuid}` + +Get doctor detail with clinics. + +**Permission:** `PUBLIC` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Doctor UUID | + +### Response `200` +```json +{ + "success": true, + "data": { + "data": { + "uuid": "550e8400-...", + "title": "دکتر علی احمدی", + "gender": "male", + "medical_system_code": "12345", + "degree": "متخصص", + "info": "...", + "image": "https://...", + "doctor_rate": 4.5, + "active_doctor_appointment": true, + "specialties": [{ "id": 1, "name": "قلب و عروق" }], + "doctor_services": [{ "id": 3, "name": "نوار قلب" }], + "clinics": [{ "uuid": "...", "name": "کلینیک الوند" }], + "created_at": 1717000000 + } + } +} +``` + +> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | + +--- + +## GET `/api/v1/doctors` + +List doctors with pagination and filters. + +**Permission:** `PUBLIC` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search in title | +| `specialty_id` | integer | ❌ | Filter by specialty | +| `city_id` | integer | ❌ | Filter by city | +| `state_id` | integer | ❌ | Filter by province | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "title": "دکتر علی احمدی", + "degree": "متخصص", + "doctor_rate": 4.5, + "image": "https://..." + } + ], + "meta": { + "totalRecords": 50, + "totalPages": 3, + "currentPage": 1 + } +} +``` + +--- + +## PATCH `/api/v1/doctor/{uuid}` + +Update doctor profile. + +**Permission:** `AUTH` — must be the owner (or `ROLE_ADMIN`) + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Doctor UUID | + +### Request Body (`application/json`) +Same fields as POST — all optional. + +### Response `200` +Updated doctor object (same structure as GET single). + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the owner | +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | + +--- + +## DELETE `/api/v1/doctor/{uuid}` + +Delete a doctor profile. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Doctor UUID | + +### Response `200` +```json +{ + "success": true, + "data": { "message": "پزشک با موفقیت حذف شد" } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | + +--- + +## POST `/file/upload/clinic_pro/doctor/field_image` + +Upload doctor profile image. + +**Permission:** `AUTH` + +### Request +`Content-Type: multipart/form-data` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file` | binary | ✅ | Image file (max 5MB) | + +### Response `200` +```json +{ + "success": true, + "data": { + "url": "https://clinic-pro.ddev.site/uploads/doctor/abc123.jpg", + "uuid": "...", + "filename": "abc123.jpg", + "filemime": "image/jpeg", + "filesize": 204800 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_FILE_001` | 422 | Invalid file type | +| `ERR_AUTH_001` | 401 | Missing token | + +--- + +## GET `/api/v1/clinic-pro/doctor-addresses/{doctorId}` + +Get all practice addresses for a doctor. + +**Permission:** `PUBLIC` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `doctorId` | integer | Doctor's numeric ID | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "id": 1, + "name": "مطب تهران", + "address": "تهران، خیابان...", + "telephone": "02112345678", + "latitude": 35.6892, + "longitude": 51.3890 + } + ] +} +``` + +--- + +## POST `/api/v1/clinic-pro/doctor-address` + +Add a new practice address. + +**Permission:** `AUTH` — must own the doctor profile + +### Request Body +```json +{ + "name": "مطب تهران", + "address": "تهران، خیابان ولیعصر", + "telephone": "02112345678", + "latitude": 35.6892, + "longitude": 51.3890 +} +``` + +| Field | Type | Required | +|-------|------|----------| +| `name` | string | ❌ | +| `address` | string | ❌ | +| `telephone` | string | ❌ | +| `latitude` | float | ❌ | +| `longitude` | float | ❌ | + +### Response `201` +```json +{ + "success": true, + "data": { + "id": 1, + "name": "مطب تهران", + "address": "تهران، خیابان ولیعصر", + "telephone": "02112345678", + "latitude": 35.6892, + "longitude": 51.3890 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the doctor owner | +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | + +--- + +## PATCH `/api/v1/clinic-pro/doctor-address/{id}` + +Update a practice address. + +**Permission:** `AUTH` — must own the doctor profile + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `id` | integer | Address ID | + +### Request Body +Same fields as POST — all optional. + +### Response `200` +Updated address object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the owner | +| `ERR_NOT_FOUND_001` | 404 | Address not found | + +--- + +## DELETE `/api/v1/clinic-pro/doctor-address/{id}` + +Delete a practice address. + +**Permission:** `AUTH` — must own the doctor profile + +### Response `200` +```json +{ "success": true, "data": { "message": "آدرس حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the owner | +| `ERR_NOT_FOUND_001` | 404 | Address not found | + +--- + +## POST `/api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}` + +Create a doctor address automatically from a clinic's location. + +**Permission:** `AUTH` — must own the doctor profile and be associated with the clinic + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `clinicUuid` | string (UUID) | Clinic UUID | + +### Response `201` +Address object created from clinic data. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_FORBIDDEN_001` | 403 | Not associated with this clinic | +| `ERR_NOT_FOUND_001` | 404 | Clinic not found | diff --git a/docs/api/insurance.md b/docs/api/insurance.md new file mode 100644 index 00000000..f1c94728 --- /dev/null +++ b/docs/api/insurance.md @@ -0,0 +1,267 @@ +# Insurance API + +> **Prefix:** `/api/v1/insurances`, `/api/v1/insurance`, `/api/v1/admin/insurance` + +Two resource types: +1. **Insurance** — master list of insurance companies managed by admin +2. **DoctorInsurance** — a doctor's acceptance of a specific insurance (with optional price) + +--- + +## GET `/api/v1/insurances` + +List all active insurances. + +**Permission:** `PUBLIC` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | ❌ | `"basic"` or `"supplementary"` | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "id": 1, + "name": "بیمه تأمین اجتماعی", + "type": "basic", + "logo_url": "https://...", + "status": "active" + }, + { + "id": 2, + "name": "بیمه ایران", + "type": "supplementary", + "logo_url": "https://...", + "status": "active" + } + ] +} +``` + +--- + +## GET `/api/v1/admin/insurances` + +List all insurances with pagination (admin view — includes inactive). + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search in name | +| `type` | string | ❌ | `"basic"` or `"supplementary"` | + +### Response `200` +```json +{ + "success": true, + "data": [ ... ], + "meta": { "totalRecords": 15, "totalPages": 1, "currentPage": 1 } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | + +--- + +## POST `/api/v1/admin/insurance` + +Create a new insurance. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "name": "بیمه تأمین اجتماعی", + "type": "basic", + "logo_url": "https://...", + "status": "active" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | ✅ | Insurance name | +| `type` | string | ✅ | `"basic"` or `"supplementary"` | +| `logo_url` | string | ❌ | Logo image URL | +| `status` | string | ❌ | `"active"` (default) or `"inactive"` | + +### Response `201` +Insurance object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_VALIDATION_002` | 422 | Missing required field | + +--- + +## PATCH `/api/v1/admin/insurance/{id}` + +Update an insurance. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `id` | integer | Insurance ID | + +All body fields optional. + +### Response `200` +Updated insurance object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Insurance not found | + +--- + +## DELETE `/api/v1/admin/insurance/{id}` + +Delete an insurance. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "بیمه حذف شد" } } +``` + +--- + +## POST `/api/v1/admin/insurance/{id}/upload-logo` + +Upload insurance logo. + +**Permission:** `ROLE_ADMIN` + +### Request +`Content-Type: multipart/form-data` + +| Field | Type | Required | +|-------|------|----------| +| `file` | binary | ✅ | + +### Response `200` +```json +{ + "success": true, + "data": { + "url": "https://...", + "uuid": "...", + "filename": "insurance_logo.png", + "filemime": "image/png", + "filesize": 51200 + } +} +``` + +--- + +## POST `/api/v1/insurance/` + +Add an insurance to a doctor's accepted list. + +**Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.create` permission) + +### Request Body (`application/json`) +```json +{ + "doctor_id": 42, + "insurance_id": 1, + "price": 150000 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `doctor_id` | integer | ✅ | Doctor's numeric ID | +| `insurance_id` | integer | ✅ | Insurance ID | +| `price` | integer | ❌ | Visit price for this insurance (Rials) | + +### Response `201` +```json +{ + "success": true, + "data": { + "id": 10, + "doctor_id": 42, + "insurance": { "id": 1, "name": "بیمه تأمین اجتماعی", "type": "basic" }, + "price": 150000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the doctor | +| `ERR_NOT_FOUND_001` | 404 | Doctor or insurance not found | +| `ERR_CONFLICT_001` | 409 | Insurance already added to doctor | + +--- + +## GET `/api/v1/insurance/{id}` + +Get a doctor-insurance link. + +**Permission:** `PUBLIC` + +### Response `200` +DoctorInsurance object. + +--- + +## PATCH `/api/v1/insurance/{id}` + +Update a doctor-insurance (e.g., change price). + +**Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.update` permission) + +### Request Body +```json +{ "price": 200000 } +``` + +### Response `200` +Updated DoctorInsurance object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the doctor | +| `ERR_NOT_FOUND_001` | 404 | Link not found | + +--- + +## DELETE `/api/v1/insurance/{id}` + +Remove an insurance from a doctor's list. + +**Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.delete` permission) + +### Response `200` +```json +{ "success": true, "data": { "message": "بیمه از لیست حذف شد" } } +``` diff --git a/docs/api/location.md b/docs/api/location.md new file mode 100644 index 00000000..32613117 --- /dev/null +++ b/docs/api/location.md @@ -0,0 +1,249 @@ +# Location API (Province & City) + +> **Prefix:** `/api/v1/provinces`, `/api/v1/cities`, `/api/v1/admin/province`, `/api/v1/admin/city` + +--- + +## GET `/api/v1/provinces` + +List all active provinces. + +**Permission:** `PUBLIC` + +### Response `200` +```json +{ + "success": true, + "data": [ + { "id": 1, "name": "تهران", "status": "active", "weight": 1 }, + { "id": 2, "name": "اصفهان", "status": "active", "weight": 2 } + ] +} +``` + +--- + +## GET `/api/v1/cities` + +List cities, optionally filtered by province. + +**Permission:** `PUBLIC` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `province_id` | integer | ❌ | Filter cities by province | + +### Response `200` +```json +{ + "success": true, + "data": [ + { "id": 10, "name": "تهران", "province_id": 1, "status": "active", "weight": 1 }, + { "id": 11, "name": "کرج", "province_id": 1, "status": "active", "weight": 2 } + ] +} +``` + +--- + +## GET `/api/v1/admin/provinces` + +List all provinces with pagination (admin view — includes inactive). + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search in name | + +### Response `200` +```json +{ + "success": true, + "data": [ ... ], + "meta": { "totalRecords": 31, "totalPages": 2, "currentPage": 1 } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | + +--- + +## GET `/api/v1/admin/cities` + +List all cities with pagination (admin view). + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search in name | +| `province_id` | integer | ❌ | Filter by province | + +### Response `200` +Paginated city list. + +--- + +## POST `/api/v1/admin/province` + +Create a new province. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "name": "تهران", + "status": "active", + "weight": 1 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | ✅ | Province name | +| `status` | string | ❌ | `"active"` (default) or `"inactive"` | +| `weight` | integer | ❌ | Sort weight | + +### Response `201` +Province object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_VALIDATION_002` | 422 | Missing name | + +--- + +## PATCH `/api/v1/admin/province/{id}` + +Update a province. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `id` | integer | Province ID | + +All fields optional. + +### Response `200` +Updated province object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Province not found | + +--- + +## DELETE `/api/v1/admin/province/{id}` + +Delete a province. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "استان حذف شد" } } +``` + +--- + +## POST `/api/v1/admin/city` + +Create a new city. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "name": "تهران", + "province_id": 1, + "status": "active", + "weight": 1 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | ✅ | City name | +| `province_id` | integer | ❌ | Parent province ID | +| `status` | string | ❌ | `"active"` or `"inactive"` | +| `weight` | integer | ❌ | Sort weight | + +### Response `201` +City object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_VALIDATION_002` | 422 | Missing name | + +--- + +## PATCH `/api/v1/admin/city/{id}` + +Update a city. + +**Permission:** `ROLE_ADMIN` + +All body fields optional. + +### Response `200` +Updated city object. + +--- + +## DELETE `/api/v1/admin/city/{id}` + +Delete a city. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "شهر حذف شد" } } +``` + +--- + +## GET `/api/v1/categorys/{bundle}` *(Legacy)* + +Legacy endpoint that proxies to the new endpoints. + +**Permission:** `PUBLIC` + +### Path Parameters +| Bundle | Maps to | +|--------|---------| +| `state` | `/api/v1/provinces` | +| `city` | `/api/v1/cities` | +| `specially_doctor` | `/api/v1/specialties` | +| `doctor_services` | `/api/v1/doctor-services` | +| `insurance_type` | `/api/v1/insurances?type=basic` | +| `supplementary_insurance` | `/api/v1/insurances?type=supplementary` | +| `tag` | `/api/v1/tags` | + +> ⚠️ Response is **triple-nested**: `data?.data?.data ?? []` +> Note: `categorys` (not `categories`) is intentional — legacy route name. diff --git a/docs/api/payment.md b/docs/api/payment.md new file mode 100644 index 00000000..65dd2e4b --- /dev/null +++ b/docs/api/payment.md @@ -0,0 +1,178 @@ +# Payment API + +> **Prefix:** `/api/v1/payment`, `/api/v1/subscription-payment` +> **Supported Gateways:** `mellat` (Mellat Bank SOAP) | `sep` (SEP REST) + +--- + +## POST `/api/v1/payment/appointment` + +Initiate payment for an appointment. Returns a redirect URL to the payment gateway. + +**Permission:** `AUTH` — must be the appointment owner (patient) + +### Request Body (`application/json`) +```json +{ + "appointment_uuid": "appt-uuid-...", + "gateway": "mellat", + "frontend_address": "https://myapp.com/payment/result" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `appointment_uuid` | string (UUID) | ✅ | Appointment to pay for | +| `gateway` | string | ✅ | `"mellat"` or `"sep"` | +| `frontend_address` | string | ❌ | Redirect URL after payment (overrides default) | + +### Response `200` +```json +{ + "success": true, + "data": { + "payment_uuid": "pay-uuid-...", + "redirect_url": "https://bpm.shaparak.ir/pgwchannel/...", + "order_id": "CLINICPRO-1717000000-ABC123" + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the patient | +| `ERR_PAYMENT_003` | 422 | Appointment not in payable state | +| `ERR_PAYMENT_002` | 422 | Invalid amount | +| `ERR_PAYMENT_001` | 503 | Payment gateway unavailable | + +--- + +## POST `/api/v1/payment/callback/{gateway}` +## GET `/api/v1/payment/callback/{gateway}` + +Payment gateway callback. Called by the bank after user completes (or cancels) payment. + +**Permission:** `PUBLIC` — called by the gateway, not the user + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `gateway` | string | `mellat` or `sep` | + +### Request (varies by gateway) +**Mellat POST fields:** +``` +ResCode=0&SaleOrderId=...&SaleReferenceId=...&RefId=... +``` + +**SEP POST fields:** +``` +Status=2&RRN=...&RefNum=...&TerminalId=...&TraceNo=... +``` + +### Response +- If `ResCode=0` (success): appointment confirmed, redirect to `frontend_address?success=1&uuid=...` +- If failed: redirect to `frontend_address?success=0&error=...` + +### Notes +- On success: appointment status → `confirmed`, wallet credited with doctor's share +- Payment record stored with: `order_id`, `amount_rials`, `status`, `gateway`, `ref_id` + +--- + +## POST `/api/v1/subscription-payment` + +Initiate a subscription / wallet top-up payment (not tied to a specific appointment). + +**Permission:** `AUTH` + +### Request Body +```json +{ + "gateway": "mellat", + "amount_rials": 1000000, + "frontend_address": "https://myapp.com/wallet/result" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `gateway` | string | ✅ | `"mellat"` or `"sep"` | +| `amount_rials` | integer | ✅ | Amount in Rials (min: 10,000) | +| `frontend_address` | string | ❌ | Redirect URL after payment | + +### Response `200` +```json +{ + "success": true, + "data": { + "payment_uuid": "pay-uuid-...", + "redirect_url": "https://bpm.shaparak.ir/pgwchannel/...", + "order_id": "CLINICPRO-SUB-1717000000-XYZ" + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_PAYMENT_002` | 422 | Invalid amount | +| `ERR_PAYMENT_001` | 503 | Gateway unavailable | + +--- + +## POST/GET `/api/v1/subscription-payment/callback/{gateway}` + +Callback for subscription payments. Same behavior as appointment callback but credits wallet instead. + +**Permission:** `PUBLIC` + +--- + +## GET `/api/v1/payment/{uuid}` + +Get payment status and details. + +**Permission:** `AUTH` — must be the payment owner or `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Payment UUID | + +### Response `200` +```json +{ + "success": true, + "data": { + "uuid": "pay-uuid-...", + "order_id": "CLINICPRO-1717000000-ABC123", + "amount_rials": 500000, + "status": "paid", + "gateway": "mellat", + "ref_id": "123456789", + "appointment_uuid": "appt-uuid-...", + "created_at": 1717000000, + "paid_at": 1717000120 + } +} +``` + +**Payment Status Values:** +| Value | Description | +|-------|-------------| +| `pending` | Created, not paid yet | +| `paid` | Successfully paid | +| `failed` | Gateway returned failure | +| `cancelled` | User cancelled at gateway | +| `refunded` | Refunded | + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the owner | +| `ERR_NOT_FOUND_001` | 404 | Payment not found | diff --git a/docs/api/rating.md b/docs/api/rating.md new file mode 100644 index 00000000..59fb1d57 --- /dev/null +++ b/docs/api/rating.md @@ -0,0 +1,275 @@ +# Rating & Comments API + +> **Prefix:** `/api/v1/rate`, `/api/v1/comment`, `/api/v1/like` + +--- + +## POST `/api/v1/rate` + +Submit a rating for a doctor. + +**Permission:** `AUTH` — any authenticated user (typically after a completed appointment) + +### Request Body (`application/json`) +```json +{ + "doctor_uuid": "550e8400-...", + "score": 5 +} +``` + +| Field | Type | Required | Validation | +|-------|------|----------|------------| +| `doctor_uuid` | string (UUID) | ✅ | Must exist | +| `score` | integer | ✅ | 1–5 | + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "rate-uuid-...", + "doctor_uuid": "...", + "score": 5, + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | +| `ERR_VALIDATION_001` | 422 | Score out of range | + +--- + +## GET `/api/v1/rate/{doctorUuid}` + +Get average rating for a doctor. + +**Permission:** `PUBLIC` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `doctorUuid` | string (UUID) | Doctor UUID | + +### Response `200` +```json +{ + "success": true, + "data": { + "average": 4.3, + "total": 47 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | + +--- + +## POST `/api/v1/comment` + +Submit a comment/review for a doctor. + +**Permission:** `AUTH` + +> Comments require admin approval before appearing publicly. + +### Request Body (`application/json`) +```json +{ + "doctor_uuid": "550e8400-...", + "body": "پزشک بسیار مؤدب و متخصص بودند" +} +``` + +| Field | Type | Required | Validation | +|-------|------|----------|------------| +| `doctor_uuid` | string (UUID) | ✅ | Must exist | +| `body` | string | ✅ | Min 10 chars | + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "comment-uuid-...", + "body": "پزشک بسیار مؤدب و متخصص بودند", + "status": "pending", + "created_at": 1717000000 + } +} +``` + +**Comment Status Values:** +| Value | Description | +|-------|-------------| +| `pending` | Awaiting admin review | +| `approved` | Visible to public | +| `rejected` | Not visible | + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | +| `ERR_VALIDATION_001` | 422 | Body too short | + +--- + +## GET `/api/v1/comments/{doctorUuid}` + +Get approved comments for a doctor. + +**Permission:** `PUBLIC` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `doctorUuid` | string (UUID) | Doctor UUID | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "body": "پزشک بسیار مؤدب...", + "user": { "uuid": "...", "real_name": "علی" }, + "likes": 3, + "status": "approved", + "created_at": 1717000000 + } + ] +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | + +--- + +## DELETE `/api/v1/comment/{uuid}` + +Delete a comment. + +**Permission:** `AUTH` — must be the comment author or `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "نظر حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the author | +| `ERR_NOT_FOUND_001` | 404 | Comment not found | + +--- + +## GET `/api/v1/admin/comments/pending` + +Get all pending comments waiting for review. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "body": "...", + "doctor": { "uuid": "...", "title": "دکتر علی احمدی" }, + "user": { "uuid": "...", "real_name": "..." }, + "status": "pending", + "created_at": 1717000000 + } + ] +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | + +--- + +## POST `/api/v1/admin/comment/{uuid}/approve` + +Approve a pending comment (makes it public). + +**Permission:** `ROLE_ADMIN` + +### Response `200` +Updated comment object with `status: "approved"`. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Comment not found | + +--- + +## POST `/api/v1/admin/comment/{uuid}/reject` + +Reject a pending comment. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +Updated comment object with `status: "rejected"`. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Comment not found | + +--- + +## POST `/api/v1/like/{commentUuid}` + +Toggle like on a comment (like if not liked, unlike if already liked). + +**Permission:** `AUTH` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `commentUuid` | string (UUID) | Comment UUID | + +### Response `200` (unlike) or `201` (new like) +```json +{ + "success": true, + "data": { + "liked": true, + "likes": 4 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_NOT_FOUND_001` | 404 | Comment not found | diff --git a/docs/api/representation.md b/docs/api/representation.md new file mode 100644 index 00000000..c8464d02 --- /dev/null +++ b/docs/api/representation.md @@ -0,0 +1,227 @@ +# Representation (Agent) API + +> **Prefix:** `/api/v1/representation` + +Representations are sales agents who earn commission on appointments booked through their referral. + +--- + +## POST `/api/v1/representation` + +Create a new representation. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "full_name": "علی احمدی", + "mobile_number": "09123456789", + "city_id": 42, + "commission_percent": 10, + "bank_account": { + "iban": "IR...", + "account_number": "1234567890", + "bank_name": "بانک ملت", + "owner_name": "علی احمدی" + } +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `full_name` | string | ✅ | Agent full name | +| `mobile_number` | string | ✅ | Login mobile (creates a User account) | +| `city_id` | integer | ❌ | City ID (FK to categories where bundle=city) | +| `commission_percent` | float | ❌ | Commission rate (0–100) | +| `bank_account` | object | ❌ | Bank details for settlements | + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "rep-uuid-...", + "full_name": "علی احمدی", + "mobile_number": "09123456789", + "city_id": 42, + "commission_percent": 10, + "active": true, + "bank_account": { ... }, + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_CONFLICT_001` | 409 | Mobile number already in use | +| `ERR_VALIDATION_001` | 422 | Invalid input | + +--- + +## GET `/api/v1/representation/{uuid}` + +Get representation detail. + +**Permission:** `AUTH` — must be the representation's user or `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Representation UUID | + +### Response `200` +```json +{ + "success": true, + "data": { + "uuid": "...", + "full_name": "علی احمدی", + "mobile_number": "09123456789", + "city_id": 42, + "city_name": "تهران", + "commission_percent": 10, + "active": true, + "bank_account": { ... }, + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not owner or admin | +| `ERR_NOT_FOUND_001` | 404 | Representation not found | + +--- + +## PATCH `/api/v1/representation/{uuid}` + +Update representation. + +**Permission:** `AUTH` — must be the representation's user or `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "full_name": "علی احمدی جدید", + "city_id": 50, + "commission_percent": 12, + "bank_account": { ... }, + "active": true +} +``` + +All fields optional. + +### Response `200` +Updated representation object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not owner or admin | +| `ERR_NOT_FOUND_001` | 404 | Representation not found | + +--- + +## DELETE `/api/v1/representation/{uuid}` + +Delete a representation. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "نماینده حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Representation not found | + +--- + +## GET `/api/v1/representation/{uuid}/dashboard/monthly` + +Get monthly earnings dashboard for a representation. + +**Permission:** `AUTH` — must be the representation's user or `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Representation UUID | + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `year` | integer | ✅ | e.g. `2024` | +| `month` | integer | ✅ | 1–12 | + +### Response `200` +```json +{ + "success": true, + "data": { + "period": { "year": 2024, "month": 6 }, + "stats": { + "total_appointments": 15, + "total_revenue_rials": 7500000, + "commission_rials": 750000, + "daily": [ + { "date": "2024-06-01", "appointments": 2, "revenue": 1000000, "commission": 100000 } + ] + } + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not owner or admin | +| `ERR_NOT_FOUND_001` | 404 | Representation not found | + +--- + +## GET `/api/v1/representation/{uuid}/dashboard/yearly` + +Get yearly earnings dashboard for a representation. + +**Permission:** `AUTH` — must be the representation's user or `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `year` | integer | ✅ | e.g. `2024` | + +### Response `200` +```json +{ + "success": true, + "data": { + "period": { "year": 2024 }, + "months": [ + { "month": 1, "appointments": 10, "revenue_rials": 5000000, "commission_rials": 500000 }, + { "month": 2, "appointments": 8, "revenue_rials": 4000000, "commission_rials": 400000 } + ], + "totals": { + "appointments": 97, + "revenue_rials": 48500000, + "commission_rials": 4850000 + } + } +} +``` diff --git a/docs/api/secretary.md b/docs/api/secretary.md new file mode 100644 index 00000000..9bead74f --- /dev/null +++ b/docs/api/secretary.md @@ -0,0 +1,220 @@ +# Secretary API + +> **Prefix:** `/api/v1/secretary`, `/api/v1/secretaries` + +Secretaries are linked to a doctor and have granular permissions controlling what they can do on behalf of the doctor. + +--- + +## POST `/api/v1/secretary` + +Create a secretary for a doctor. + +**Permission:** `ROLE_DOCTOR` — must own the doctor profile + +### Request Body (`application/json`) +```json +{ + "doctor_uuid": "550e8400-...", + "mobile_number": "09123456789", + "password": "secretaryPass123", + "permissions": { + "version": 1, + "resources": { + "appointments": { "view": true, "create": true, "cancel": false, "update_status": true }, + "addresses": { "view": true, "create": false, "update": false, "delete": false }, + "clinic_info": { "view": true, "update": false }, + "insurances": { "view": true, "create": false, "update": false, "delete": false } + } + } +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `doctor_uuid` | string (UUID) | ✅ | Doctor to assign secretary to | +| `mobile_number` | string | ✅ | Secretary's login mobile | +| `password` | string | ❌ | Initial password (auto-generated if omitted) | +| `permissions` | object | ❌ | Permission set (see structure below) | + +**Permissions Structure:** +```json +{ + "version": 1, + "resources": { + "appointments": { + "view": true, // Can view appointments list + "create": true, // Can book appointments + "cancel": false, // Can cancel appointments + "update_status": true // Can mark as completed/no_show + }, + "addresses": { + "view": true, + "create": false, + "update": false, + "delete": false + }, + "clinic_info": { + "view": true, + "update": false + }, + "insurances": { + "view": true, + "create": false, + "update": false, + "delete": false + } + } +} +``` + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "sec-uuid-...", + "mobile_number": "09123456789", + "active": true, + "permissions": { ... }, + "doctor": { "uuid": "...", "title": "دکتر علی احمدی" }, + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not a doctor or not the doctor's owner | +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | +| `ERR_CONFLICT_001` | 409 | Mobile number already in use | +| `ERR_SECRETARY_001` | 422 | Plan limit for secretaries reached | + +--- + +## GET `/api/v1/secretary/{uuid}` + +Get secretary detail. + +**Permission:** `AUTH` — must be the linked doctor or `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Secretary UUID | + +### Response `200` +```json +{ + "success": true, + "data": { + "uuid": "...", + "mobile_number": "09123456789", + "active": true, + "permissions": { ... }, + "doctor": { "uuid": "...", "title": "دکتر علی احمدی" }, + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not authorized | +| `ERR_NOT_FOUND_001` | 404 | Secretary not found | + +--- + +## PATCH `/api/v1/secretary/{uuid}` + +Update secretary active status or permissions. + +**Permission:** `ROLE_DOCTOR` — must be the linked doctor + +### Request Body (`application/json`) +```json +{ + "active": false, + "permissions": { + "version": 1, + "resources": { + "appointments": { "view": true, "create": false, "cancel": false, "update_status": false } + } + } +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `active` | boolean | ❌ | Enable/disable secretary | +| `permissions` | object | ❌ | New permissions object | + +### Response `200` +Updated secretary object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the linked doctor | +| `ERR_NOT_FOUND_001` | 404 | Secretary not found | + +--- + +## DELETE `/api/v1/secretary/{uuid}` + +Delete a secretary. + +**Permission:** `ROLE_DOCTOR` — must be the linked doctor + +### Response `200` +```json +{ "success": true, "data": { "message": "منشی حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the linked doctor | +| `ERR_NOT_FOUND_001` | 404 | Secretary not found | + +--- + +## GET `/api/v1/secretaries/{doctorUuid}` + +Get all secretaries for a specific doctor. + +**Permission:** `AUTH` — must be the doctor or `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `doctorUuid` | string (UUID) | Doctor UUID | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "mobile_number": "09...", + "active": true, + "permissions": { ... }, + "created_at": 1717000000 + } + ] +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the doctor | +| `ERR_NOT_FOUND_001` | 404 | Doctor not found | diff --git a/docs/api/settlement.md b/docs/api/settlement.md new file mode 100644 index 00000000..f6545e5d --- /dev/null +++ b/docs/api/settlement.md @@ -0,0 +1,254 @@ +# Settlement & Wallet API + +> **Prefix:** `/api/v1/wallet`, `/api/v1/settlement` + +--- + +## GET `/api/v1/wallet/balance` + +Get authenticated user's wallet balance. + +**Permission:** `AUTH` + +### Response `200` +```json +{ + "success": true, + "data": { + "balance_rials": 2500000, + "recent_transactions": [ + { + "uuid": "...", + "type": "credit", + "amount_rials": 500000, + "balance_after": 2500000, + "description": "دریافت از نوبت شماره ...", + "created_at": 1717000000 + } + ] + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | + +--- + +## GET `/api/v1/wallet/transactions` + +Get wallet transaction history for the authenticated user. + +**Permission:** `AUTH` + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "type": "credit", + "amount_rials": 500000, + "balance_after": 2500000, + "description": "دریافت از نوبت", + "created_at": 1717000000 + }, + { + "uuid": "...", + "type": "debit", + "amount_rials": 200000, + "balance_after": 2300000, + "description": "تسویه‌حساب", + "created_at": 1716900000 + } + ] +} +``` + +**Transaction Type Values:** +| Value | Description | +|-------|-------------| +| `credit` | Money added to wallet | +| `debit` | Money removed from wallet | + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | + +--- + +## POST `/api/v1/settlement` + +Request a settlement (withdrawal from wallet to bank account). + +**Permission:** `AUTH` + +### Request Body (`application/json`) +```json +{ + "amount_rials": 1000000, + "bank_account": { + "iban": "IR...", + "account_number": "1234567890", + "bank_name": "بانک ملت", + "owner_name": "علی احمدی" + } +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `amount_rials` | integer | ✅ | Amount to withdraw (must be ≤ wallet balance) | +| `bank_account` | object | ❌ | Bank account details (saved if not previously set) | +| `bank_account.iban` | string | ❌ | IBAN (شبا) | +| `bank_account.account_number` | string | ❌ | Account number | +| `bank_account.bank_name` | string | ❌ | Bank name | +| `bank_account.owner_name` | string | ❌ | Account owner name | + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "settle-uuid-...", + "amount_rials": 1000000, + "status": "pending", + "bank_account": { + "iban": "IR...", + "bank_name": "بانک ملت", + "owner_name": "علی احمدی" + }, + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_VALIDATION_001` | 422 | Amount exceeds balance or invalid amount | + +--- + +## GET `/api/v1/settlement` + +Get authenticated user's settlement requests. + +**Permission:** `AUTH` + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "amount_rials": 1000000, + "status": "pending", + "note": null, + "created_at": 1717000000, + "processed_at": null + } + ] +} +``` + +**Settlement Status Values:** +| Value | Description | +|-------|-------------| +| `pending` | Awaiting admin review | +| `approved` | Approved, payment sent | +| `rejected` | Rejected by admin | + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | + +--- + +## GET `/api/v1/settlement/{uuid}` + +Get a single settlement. + +**Permission:** `AUTH` — must be the owner or `ROLE_ADMIN` + +### Response `200` +Settlement object with full details including `bank_account`. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the owner | +| `ERR_NOT_FOUND_001` | 404 | Settlement not found | + +--- + +## POST `/api/v1/settlement/{uuid}/approve` + +Approve a settlement request. Marks it as paid and debits the wallet. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Settlement UUID | + +### Request Body (`application/json`) +```json +{ + "note": "پرداخت شد — شناسه پیگیری: 123456" +} +``` + +| Field | Type | Required | +|-------|------|----------| +| `note` | string | ❌ | + +### Response `200` +Updated settlement object with `status: "approved"`. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Settlement not found | +| `ERR_VALIDATION_001` | 422 | Already processed | + +--- + +## POST `/api/v1/settlement/{uuid}/reject` + +Reject a settlement request. Returns the amount back to wallet. + +**Permission:** `ROLE_ADMIN` + +### Request Body +```json +{ + "note": "حساب بانکی نادرست است" +} +``` + +| Field | Type | Required | +|-------|------|----------| +| `note` | string | ✅ | Rejection reason (required) | + +### Response `200` +Updated settlement object with `status: "rejected"`. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Settlement not found | +| `ERR_VALIDATION_002` | 422 | Missing note | diff --git a/docs/api/sms.md b/docs/api/sms.md new file mode 100644 index 00000000..1cad529f --- /dev/null +++ b/docs/api/sms.md @@ -0,0 +1,300 @@ +# SMS API + +> **Prefix:** `/api/v1/sms` +> **Providers:** `kavenegar` (default) | `rangineh` +> All send operations are dispatched **asynchronously** via Symfony Messenger → Redis queue. + +--- + +## POST `/api/v1/sms/send` + +Send a direct SMS message (free text). + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "mobile": "09123456789", + "message": "سلام، پیام آزمایشی", + "provider": "kavenegar" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `mobile` | string | ✅ | Recipient mobile (`09XXXXXXXXX`) | +| `message` | string | ✅ | Message text | +| `provider` | string | ❌ | `"kavenegar"` or `"rangineh"` (default from env) | + +### Response `200` +```json +{ + "success": true, + "data": { "message": "پیامک با موفقیت ارسال شد" } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_VALIDATION_001` | 422 | Invalid mobile format | + +--- + +## POST `/api/v1/sms/send-template` + +Send an SMS using an approved template. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "mobile": "09123456789", + "template_uuid": "tmpl-uuid-...", + "vars": { + "name": "دکتر علی احمدی", + "date": "۱۵ خرداد ۱۴۰۴" + }, + "provider": "kavenegar" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `mobile` | string | ✅ | Recipient mobile | +| `template_uuid` | string (UUID) | ✅ | UUID of an approved template | +| `vars` | object | ❌ | Key-value substitutions for template placeholders | +| `provider` | string | ❌ | Override provider | + +### Response `200` +```json +{ + "success": true, + "data": { "message": "پیامک با موفقیت ارسال شد" } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Template not found | +| `ERR_VALIDATION_001` | 422 | Template not approved | + +--- + +## POST `/api/v1/sms/template` + +Create a new SMS template. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "name": "تأیید نوبت", + "body": "دکتر گرامی ${name}، نوبت شما در تاریخ ${date} تأیید شد.", + "variables": ["name", "date"] +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | ✅ | Template display name | +| `body` | string | ✅ | Template text with `${variable}` placeholders | +| `variables` | string[] | ❌ | List of expected variable names | + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "tmpl-uuid-...", + "name": "تأیید نوبت", + "body": "دکتر گرامی ${name}...", + "variables": ["name", "date"], + "status": "draft", + "created_at": 1717000000 + } +} +``` + +**Template Status Values:** +| Value | Description | +|-------|-------------| +| `draft` | Created, not submitted | +| `pending` | Submitted for review | +| `approved` | Ready to use | +| `rejected` | Rejected | + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_VALIDATION_002` | 422 | Missing required field | + +--- + +## GET `/api/v1/sms/template/{uuid}` + +Get template detail. + +**Permission:** `AUTH` + +### Response `200` +Template object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_NOT_FOUND_001` | 404 | Template not found | + +--- + +## PATCH `/api/v1/sms/template/{uuid}` + +Update a template (only allowed in `draft` or `rejected` status). + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "name": "تأیید نوبت - ویرایش", + "body": "نوبت شما در ${date} تأیید شد.", + "variables": ["date"] +} +``` + +All fields optional. + +### Response `200` +Updated template object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Template not found | +| `ERR_SMS_003` | 422 | Template already submitted/approved | + +--- + +## POST `/api/v1/sms/template/{uuid}/submit` + +Submit template for admin review (moves status from `draft` to `pending`). + +**Permission:** `ROLE_ADMIN` + +### Response `200` +Updated template with `status: "pending"`. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Template not found | +| `ERR_SMS_003` | 422 | Already submitted | + +--- + +## DELETE `/api/v1/sms/template/{uuid}` + +Delete a template. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "قالب پیامک حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Template not found | + +--- + +## GET `/api/v1/admin/sms/templates` + +List all templates (admin view with all statuses). + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "uuid": "...", + "name": "تأیید نوبت", + "status": "approved", + "created_at": 1717000000 + } + ] +} +``` + +--- + +## POST `/api/v1/admin/sms/template/{uuid}/approve` + +Approve a pending template. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "note": "تأیید شد", + "provider_code": "verify_appointment" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `note` | string | ❌ | Admin note | +| `provider_code` | string | ❌ | Provider-side template code | + +### Response `200` +Updated template with `status: "approved"`. + +--- + +## POST `/api/v1/admin/sms/template/{uuid}/reject` + +Reject a pending template. + +**Permission:** `ROLE_ADMIN` + +### Request Body +```json +{ + "note": "متن قالب نامناسب است" +} +``` + +| Field | Type | Required | +|-------|------|----------| +| `note` | string | ✅ | + +### Response `200` +Updated template with `status: "rejected"`. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_VALIDATION_002` | 422 | Missing note | diff --git a/docs/api/specialty.md b/docs/api/specialty.md new file mode 100644 index 00000000..dd250973 --- /dev/null +++ b/docs/api/specialty.md @@ -0,0 +1,162 @@ +# Specialty API + +> **Prefix:** `/api/v1/specialties`, `/api/v1/admin/specialty` + +--- + +## GET `/api/v1/specialties` + +List all medical specialties. + +**Permission:** `PUBLIC` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `parent_id` | integer | ❌ | Filter to sub-specialties of a parent | + +### Response `200` +```json +{ + "success": true, + "data": [ + { + "id": 1, + "name": "قلب و عروق", + "slug": "ghalb-va-oroug", + "parent_id": null, + "status": "active", + "weight": 10 + }, + { + "id": 5, + "name": "فوق تخصص قلب", + "slug": "fowgh-takhassos-ghalb", + "parent_id": 1, + "status": "active", + "weight": 5 + } + ] +} +``` + +--- + +## GET `/api/v1/admin/specialties` + +List all specialties with pagination (admin view — includes inactive). + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search in name | + +### Response `200` +```json +{ + "success": true, + "data": [ ... ], + "meta": { "totalRecords": 50, "totalPages": 3, "currentPage": 1 } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | + +--- + +## POST `/api/v1/admin/specialty` + +Create a new specialty. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "name": "قلب و عروق", + "slug": "ghalb-va-oroug", + "parent_id": null, + "status": "active", + "weight": 10 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | ✅ | Specialty name | +| `slug` | string | ❌ | Auto-generated from name if omitted | +| `parent_id` | integer | ❌ | Parent specialty ID (for sub-specialties) | +| `status` | string | ❌ | `"active"` (default) or `"inactive"` | +| `weight` | integer | ❌ | Sort weight | + +### Response `201` +Specialty object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_VALIDATION_002` | 422 | Missing name | + +--- + +## PATCH `/api/v1/admin/specialty/{id}` + +Update a specialty. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `id` | integer | Specialty ID | + +### Request Body (all optional) +```json +{ + "name": "قلب و عروق ویرایش‌شده", + "status": "inactive", + "weight": 20, + "parent_id": null, + "slug": "new-slug" +} +``` + +### Response `200` +Updated specialty object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Specialty not found | + +--- + +## DELETE `/api/v1/admin/specialty/{id}` + +Delete a specialty. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "تخصص حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Specialty not found | diff --git a/docs/api/tag.md b/docs/api/tag.md new file mode 100644 index 00000000..15842a89 --- /dev/null +++ b/docs/api/tag.md @@ -0,0 +1,132 @@ +# Tag API + +> **Prefix:** `/api/v1/tags`, `/api/v1/admin/tag` + +Tags are used for categorizing blog posts. + +--- + +## GET `/api/v1/tags` + +List all active tags. + +**Permission:** `PUBLIC` + +### Response `200` +```json +{ + "success": true, + "data": [ + { "id": 1, "name": "دیابت", "slug": "diabat", "status": "active" }, + { "id": 2, "name": "قلب", "slug": "ghalb", "status": "active" } + ] +} +``` + +--- + +## GET `/api/v1/admin/tags` + +List all tags with pagination (admin view — includes inactive). + +**Permission:** `ROLE_ADMIN` + +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 20 | +| `search` | string | ❌ | Search in name | + +### Response `200` +```json +{ + "success": true, + "data": [ ... ], + "meta": { "totalRecords": 10, "totalPages": 1, "currentPage": 1 } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | + +--- + +## POST `/api/v1/admin/tag` + +Create a new tag. + +**Permission:** `ROLE_ADMIN` + +### Request Body (`application/json`) +```json +{ + "name": "دیابت", + "slug": "diabat", + "status": "active" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | ✅ | Tag name | +| `slug` | string | ❌ | Auto-generated from name if omitted | +| `status` | string | ❌ | `"active"` (default) or `"inactive"` | + +### Response `201` +Tag object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_VALIDATION_002` | 422 | Missing name | + +--- + +## PATCH `/api/v1/admin/tag/{id}` + +Update a tag. + +**Permission:** `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `id` | integer | Tag ID | + +All fields optional. + +### Response `200` +Updated tag object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Tag not found | + +--- + +## DELETE `/api/v1/admin/tag/{id}` + +Delete a tag. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "تگ حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Tag not found | diff --git a/docs/api/user-profile.md b/docs/api/user-profile.md new file mode 100644 index 00000000..610d0ff6 --- /dev/null +++ b/docs/api/user-profile.md @@ -0,0 +1,154 @@ +# User Profile API + +> **Prefix:** `/api/v1/user-profile` + +User medical profiles store health information that can be shared with doctors. + +--- + +## POST `/api/v1/user-profile` + +Create a medical profile for the authenticated user. + +**Permission:** `AUTH` + +### Request Body (`application/json`) +```json +{ + "name": "علی", + "family": "احمدی", + "fathers_name": "محمد", + "national_code": "0012345678", + "gender": "male", + "blood_type": "A+", + "marital_status": "married", + "education": "لیسانس", + "job": "مهندس", + "address": "تهران، خیابان ولیعصر", + "home_phone": "02112345678", + "work_phone": "02198765432", + "insurance_id": 1, + "description": "توضیحات بیشتر", + "sharing_with_user": true, + "birthday": "1370-06-15", + "date_of_birth": 648172800, + "basic_insurance": "بیمه تأمین اجتماعی", + "supplementary_insurance": "بیمه ایران", + "other": {} +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | ❌ | First name | +| `family` | string | ❌ | Last name | +| `fathers_name` | string | ❌ | Father's name | +| `national_code` | string | ❌ | National ID (10 digits) | +| `gender` | string | ❌ | `"male"` or `"female"` | +| `blood_type` | string | ❌ | `"A+"`, `"A-"`, `"B+"`, `"B-"`, `"AB+"`, `"AB-"`, `"O+"`, `"O-"` | +| `marital_status` | string | ❌ | `"single"` or `"married"` | +| `education` | string | ❌ | Education level | +| `job` | string | ❌ | Occupation | +| `address` | string | ❌ | Home address | +| `home_phone` | string | ❌ | Home phone number | +| `work_phone` | string | ❌ | Work phone number | +| `insurance_id` | integer | ❌ | Insurance ID | +| `description` | string | ❌ | Additional notes | +| `sharing_with_user` | boolean | ❌ | Allow doctors to view this profile | +| `birthday` | string | ❌ | Birthday in Jalali format `YYYY-MM-DD` | +| `date_of_birth` | integer | ❌ | Birthday as Unix timestamp | +| `basic_insurance` | string | ❌ | Basic insurance name | +| `supplementary_insurance` | string | ❌ | Supplementary insurance name | +| `other` | object | ❌ | Extra metadata | + +### Response `201` +```json +{ + "success": true, + "data": { + "uuid": "profile-uuid-...", + "name": "علی", + "family": "احمدی", + "gender": "male", + "blood_type": "A+", + "national_code": "0012345678", + "sharing_with_user": true, + "created_at": 1717000000 + } +} +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_CONFLICT_001` | 409 | Profile already exists for this user | + +--- + +## GET `/api/v1/user-profile/{uuid}` + +Get a user profile. + +**Permission:** `AUTH` — must be the profile owner, or a doctor if `sharing_with_user: true`, or `ROLE_ADMIN` + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Profile UUID | + +### Response `200` +Full profile object including all fields. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not authorized to view this profile | +| `ERR_NOT_FOUND_001` | 404 | Profile not found | + +--- + +## PATCH `/api/v1/user-profile/{uuid}` + +Update a user profile. + +**Permission:** `AUTH` — must be the profile owner + +### Path Parameters +| Param | Type | Description | +|-------|------|-------------| +| `uuid` | string (UUID) | Profile UUID | + +### Request Body +Same fields as POST — all optional. + +### Response `200` +Updated profile object. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_FORBIDDEN_001` | 403 | Not the profile owner | +| `ERR_NOT_FOUND_001` | 404 | Profile not found | + +--- + +## DELETE `/api/v1/user-profile/{uuid}` + +Delete a user profile. + +**Permission:** `ROLE_ADMIN` + +### Response `200` +```json +{ "success": true, "data": { "message": "پروفایل حذف شد" } } +``` + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_AUTH_001` | 401 | Missing token | +| `ERR_AUTH_006` | 403 | Not admin | +| `ERR_NOT_FOUND_001` | 404 | Profile not found |