1. شارژ کیف پول is now functional end-to-end. New owner-gated
POST /api/v1/patient/{uuid}/wallet/charge creates a manual credit
WalletTransaction (computed balance_after); the patient detail's wallet tab
gains a top-up modal (PriceInput + description) and supports ?tab= deep
links. The deposit sections of the create drawer, the edit page and the
replace modal link to it via WalletChargeLink (record resolved by mobile).
2. جایگزینی نوبت now matches appointments-replace.pdf: patient search-or-new,
بخش/سرویس/پرسنل selects prefilled from the appointment, deposit toggle +
amount + charge link, read-only original date/time, status pick and notes —
all through the general PATCH.
3. The confirmed-appointments table is paginated (20/page, client-side so the
schedule view and doctor-tab derivation keep the whole day), resetting on
date/doctor/filter changes. The page-local STATUS_META also adopts the
design labels plus following_up/salon for the schedule cards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
623 lines
26 KiB
Markdown
623 lines
26 KiB
Markdown
# Patient Records & Sessions API
|
||
|
||
## Overview
|
||
|
||
Patient records track patients per entity (doctor or clinic). Each record holds multiple sessions (visits). Access requires an active subscription with the `patient_records` feature.
|
||
|
||
**Base path:** `/api/v1`
|
||
**Auth:** Bearer JWT (doctor, clinic, or secretary with `appointments.view` permission required)
|
||
|
||
---
|
||
|
||
## Endpoints
|
||
|
||
### List Patients
|
||
|
||
```
|
||
GET /api/v1/patients
|
||
```
|
||
|
||
Returns a paginated list of patient records belonging to the authenticated entity.
|
||
|
||
**Query params:**
|
||
|
||
| Param | Type | Default | Description |
|
||
|-------|------|---------|-------------|
|
||
| `page` | int | 1 | Page number |
|
||
| `limit` | int | 20 | Items per page (10–50) |
|
||
| `search` | string | — | Search by patient name or phone |
|
||
|
||
**Response 200:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"entity_type": "doctor",
|
||
"entity_id": 5,
|
||
"user": { "uuid": "...", "fullName": "علی رضایی", "phone": "09123456789" },
|
||
"created_by_type": "doctor",
|
||
"created_by_id": 5,
|
||
"created_at": 1718375000
|
||
}
|
||
],
|
||
"meta": {
|
||
"totalRecords": 42,
|
||
"totalPages": 3,
|
||
"currentPage": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No active plan with `patient_records` feature |
|
||
|
||
---
|
||
|
||
### Create Patient Record
|
||
|
||
```
|
||
POST /api/v1/patient
|
||
```
|
||
|
||
Creates a patient record for a user under the current entity. If the record already exists, returns the existing record (idempotent).
|
||
|
||
سه حالت پشتیبانی میشود:
|
||
1. **کاربر ثبتنامکرده با uuid:** `user_uuid` ارسال شود.
|
||
2. **کاربر ثبتنامکرده با موبایل:** `mobile` ارسال شود (کاربر موجود پیدا میشود).
|
||
3. **بیمار جدید بدون ثبتنام:** `mobile` + `name` ارسال شود؛ اگر کاربری با آن موبایل نباشد، `User` جدید (نقش `ROLE_USER`، بدون رمز عبور) ساخته میشود سپس پرونده.
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{
|
||
"user_uuid": "string (اختیاری)",
|
||
"mobile": "09xxxxxxxxx (اختیاری — برای جستجو یا ساخت بیمار جدید)",
|
||
"name": "string (الزامی فقط هنگام ساخت بیمار جدید)",
|
||
"national_code": "string (اختیاری، ۱۰ رقم)",
|
||
"record_number": "string (اختیاری) — شماره پرونده، مخصوص رکورد",
|
||
"tags": ["uuid برچسبهای TenantTag (اختیاری) — باید متعلق به همین tenant باشند"]
|
||
}
|
||
```
|
||
|
||
- اگر `user_uuid` و `mobile` هر دو خالی باشند → خطا.
|
||
- `record_number` و `tags` روی خودِ رکورد ذخیره میشوند (نه پروفایل کاربر). سایر مشخصات دموگرافیک (`gender`, `date_of_birth`, `referral_source`, `description`, بیمهها) روی `UserProfile` هستند و از طریق `PATCH /patient/{uuid}` ست میشوند. پاسخ همیشه `record_number` و `tags: [{uuid,name,color}]` را برمیگرداند.
|
||
- برچسب متعلق به tenant دیگر → `422 ERR_VALIDATION_001` (`field: tags`).
|
||
- `national_code` فقط وقتی روی کاربر ست میشود که کاربر کد ملی نداشته باشد.
|
||
- موبایل تکراری duplicate نمیسازد؛ همان کاربر استفاده میشود.
|
||
- **یکتایی کد ملی:** اگر `national_code` ارسالی قبلاً به پروفایل کاربر دیگری تعلق داشته باشد → `409` با کد `ERR_PROFILE_001` (`field: national_code`). پیام خطا شامل شماره موبایلِ ماسکشدهی صاحب کد است (مثلاً «این کد ملی قبلاً با شماره 0912****56 ثبت شده است»). یک کد ملی = یک بیمار در کل سیستم (همراستا با قید یکتای `profiles.national_code`).
|
||
|
||
**Response 201:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"uuid": "...",
|
||
"entity_type": "doctor",
|
||
"entity_id": 5,
|
||
"user": { "uuid": "...", "fullName": "...", "phone": "..." },
|
||
"created_by_type": "doctor",
|
||
"created_by_id": 5,
|
||
"created_at": 1718375000
|
||
}
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_VALIDATION_001` | 422 | `user_uuid`/`mobile` خالی، یا موبایل/کد ملی نامعتبر، یا نام برای بیمار جدید خالی |
|
||
| `ERR_PROFILE_001` | 409 | کد ملی قبلاً برای پروفایل کاربر دیگری ثبت شده (`field: national_code`) |
|
||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||
|
||
---
|
||
|
||
### Get Patient Record
|
||
|
||
```
|
||
GET /api/v1/patient/{uuid}
|
||
```
|
||
|
||
Returns a single patient record, enriched with the patient's full profile (`profile`) درونخطی از `UserProfile`. اگر پروفایل وجود نداشت، فیلدها `null` برمیگردند (نه خطا). نام بیمهها از روی id resolve میشوند.
|
||
|
||
**Response 200:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"uuid": "...",
|
||
"entity_type": "doctor",
|
||
"entity_id": 5,
|
||
"user_uuid": "...",
|
||
"user_name": "...",
|
||
"user_mobile": "0912...",
|
||
"user_national_code": "...",
|
||
"created_at": 1718375000,
|
||
"profile": {
|
||
"full_name": "محمد محمدی",
|
||
"name": "محمد",
|
||
"family": "محمدی",
|
||
"fathers_name": "رضا",
|
||
"national_code": "0012345678",
|
||
"gender": "male",
|
||
"date_of_birth": 700000000,
|
||
"blood_type": "O+",
|
||
"marital_status": "single",
|
||
"education": "کارشناسی",
|
||
"field_of_study": "نرمافزار",
|
||
"job": "...",
|
||
"address": "...",
|
||
"province_id": 8,
|
||
"city_id": 42,
|
||
"postal_code": "8913746351",
|
||
"referral_source": "اینستاگرام",
|
||
"description": "...",
|
||
"home_phone": "...",
|
||
"work_phone": "...",
|
||
"mobile": "0912...",
|
||
"basic_insurance_id": 3,
|
||
"basic_insurance_name": "تأمین اجتماعی",
|
||
"supplementary_insurance_id": 9,
|
||
"supplementary_insurance_name": "دانا"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
> `date_of_birth` یک Unix timestamp است؛ سمت کلاینت با `formatDate()` شمسی نمایش داده میشود. `profile` برای هر دو `entity_type` (doctor/clinic) یکسان است.
|
||
|
||
**Errors:**
|
||
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_PATIENT_NOT_FOUND` | 404 | Record not found or not owned by caller |
|
||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||
|
||
---
|
||
|
||
### Update Patient Basic Info
|
||
|
||
```
|
||
PATCH /api/v1/patient/{uuid}
|
||
```
|
||
|
||
اطلاعات پایهی بیمار را بهروزرسانی میکند. برای هر سه نقشِ صاحبِ پرونده در دسترس است: **پزشک، کلینیک، و منشیِ فعالِ همان مطب/کلینیک** (دسترسی از طریق همان `resolveEntity` + `assertPatientGate` مثل بقیهی endpointهای بیمار کنترل میشود؛ منشی باید `db_uuid` فعال داشته باشد).
|
||
|
||
بهروزرسانی **partial** است — فقط کلیدهای ارسالشده اعمال میشوند. مقدار `""`/`null` برای فیلدهای پروفایل یعنی «پاککردن». `name` روی `User.realName` و بقیهی فیلدها روی `UserProfile` مینشینند (در صورت نبود پروفایل، ساخته میشود).
|
||
|
||
> **شماره موبایل قابل ویرایش است** — موبایل همان شناسهی ورود کاربر است، پس ارسال `mobile` علاوه بر شمارهی تماس، **نامکاربری ورود کاربر را نیز تغییر میدهد**. باید `^09\d{9}$` و در سطح کاربران یکتا باشد.
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{
|
||
"name": "محمد",
|
||
"family": "محمدی",
|
||
"fathers_name": "رضا",
|
||
"mobile": "09131234567",
|
||
"national_code": "0012345678",
|
||
"gender": "male",
|
||
"blood_type": "O+",
|
||
"marital_status": "single",
|
||
"education": "کارشناسی",
|
||
"field_of_study": "نرمافزار",
|
||
"job": "مهندس",
|
||
"home_phone": "03511111111",
|
||
"work_phone": "03512222222",
|
||
"address": "...",
|
||
"province_id": 8,
|
||
"city_id": 42,
|
||
"postal_code": "8913746351",
|
||
"referral_source": "اینستاگرام",
|
||
"description": "...",
|
||
"basic_insurance_id": 3,
|
||
"supplementary_insurance_id": 9
|
||
}
|
||
```
|
||
|
||
| Field | Type | Notes |
|
||
|-------|------|-------|
|
||
| `name` | string | اگر ارسال شود و خالی نباشد → `User.realName`. رشتهی خالی نادیده گرفته میشود. |
|
||
| `family` | string\|null | `UserProfile.family` |
|
||
| `fathers_name` | string\|null | `UserProfile.fathersName` (نام پدر) |
|
||
| `mobile` | string\|null | اگر خالی نباشد و با موبایل فعلی فرق کند باید `^09\d{9}$` و یکتا باشد؛ روی `User.mobileNumber` ست میشود و **شناسهی ورود** را عوض میکند |
|
||
| `national_code` | string\|null | اگر خالی نباشد باید ۱۰ رقم و در سطح بیمار یکتا باشد؛ روی `User.nationalCode` و `UserProfile.nationalCode` ست میشود |
|
||
| `gender` | `male`\|`female`\|null | |
|
||
| `blood_type` | string\|null | |
|
||
| `marital_status` | string\|null | |
|
||
| `education` | string\|null | مقطع تحصیلی (`UserProfile.education`) |
|
||
| `field_of_study` | string\|null | رشتهی تحصیلی (`UserProfile.fieldOfStudy`) |
|
||
| `job` | string\|null | |
|
||
| `home_phone`, `work_phone` | string\|null | |
|
||
| `address` | string\|null | |
|
||
| `province_id`, `city_id` | int\|null | id استان/شهر (category؛ `null` = حذف) |
|
||
| `postal_code` | string\|null | کد پستی |
|
||
| `referral_source` | string\|null | نحوهی آشنایی |
|
||
| `description` | string\|null | توضیحات |
|
||
| `basic_insurance_id`, `supplementary_insurance_id` | int\|null | id بیمه؛ `null` = حذف |
|
||
|
||
**Response 200:** مثل `GET /api/v1/patient/{uuid}` (رکورد + `profile` تازه).
|
||
|
||
**Errors:**
|
||
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_PATIENT_NOT_FOUND` | 404 | Record not found or not owned by caller |
|
||
| `ERR_VALIDATION_001` | 422 | کد ملی باید ۱۰ رقم باشد (`field: national_code`) یا موبایل نامعتبر است (`field: mobile`) |
|
||
| `ERR_PROFILE_NATIONAL_CODE_TAKEN` | 409 | کد ملی متعلق به بیمار دیگری است (`field: national_code`) |
|
||
| `ERR_PROFILE_002` | 409 | موبایل متعلق به کاربر دیگری است (`field: mobile`) |
|
||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||
|
||
---
|
||
|
||
### List Patient Sessions
|
||
|
||
```
|
||
GET /api/v1/patient/{uuid}/sessions
|
||
```
|
||
|
||
Returns paginated sessions for a patient record.
|
||
|
||
**Query params:** `page`, `limit` (same as list)
|
||
|
||
**Response 200:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"record_uuid": "...",
|
||
"appointment_uuid": null,
|
||
"insurance_base_id": null,
|
||
"insurance_supplementary_id": null,
|
||
"visit_price_rials": 200000,
|
||
"base_insurance_discount_percent": "10.00",
|
||
"supplementary_discount_percent": "5.00",
|
||
"services_total_rials": 50000,
|
||
"final_price_rials": 230000,
|
||
"payment_method": "cash",
|
||
"is_paid": true,
|
||
"services": [
|
||
{
|
||
"uuid": "...",
|
||
"service_item_uuid": "...",
|
||
"service_name": "کندلا ۲۰۲۱",
|
||
"staff_uuid": "...",
|
||
"staff_name": "ژیلا فتحی",
|
||
"price_rials": 50000,
|
||
"quantity": 1,
|
||
"line_total_rials": 50000,
|
||
"created_at": 1718375000
|
||
}
|
||
],
|
||
"invoice_uuid": "...",
|
||
"invoice_status": "finalized",
|
||
"patient_debt_rials": 0,
|
||
"notes": "...",
|
||
"created_at": 1718375000,
|
||
"updated_at": 1718375000
|
||
}
|
||
],
|
||
"meta": { "totalRecords": 8, "totalPages": 1, "currentPage": 1 }
|
||
}
|
||
```
|
||
|
||
**فیلدهای غنیسازیشده (برای تب «سرویسها»):**
|
||
|
||
| Field | Type | Notes |
|
||
|-------|------|-------|
|
||
| `is_paid` | bool | `true` وقتی `payment_method !== "pending"` |
|
||
| `services` | array | سرویسهای ثبتشده در این session (نام، انجامدهنده/`staff`, قیمت، تعداد) |
|
||
| `invoice_uuid` | string\|null | uuid فاکتور مرتبط (اگر ساخته شده باشد؛ برای «مشاهده فاکتور») |
|
||
| `invoice_status` | string\|null | `draft`\|`finalized`\|`paid`\|`void` |
|
||
| `patient_debt_rials` | int | مانده بدهی سهم بیمار؛ `0` اگر تسویه شده، وگرنه سهم بیمارِ فاکتور یا `final_price_rials` |
|
||
|
||
> **ثبت پرداخت («تکمیل پرداخت»):** از همان `PATCH /api/v1/session/{uuid}` با بدنهی `{ "payment_method": "cash" }` استفاده میشود؛ پس از آن `is_paid=true` و `patient_debt_rials=0` میشود. مشاهدهی فاکتور از `GET /api/v1/billing/invoices/{invoice_uuid}` (این endpoint اکنون برای منشیِ فعال هم در دسترس است).
|
||
|
||
**Errors:**
|
||
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_PATIENT_NOT_FOUND` | 404 | Record not found or not owned by caller |
|
||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||
|
||
---
|
||
|
||
### List Patient Appointments
|
||
|
||
```
|
||
GET /api/v1/patient/{uuid}/appointments
|
||
```
|
||
|
||
نوبتهای همین بیمار را برمیگرداند. برای جلوگیری از نشتِ اطلاعات بین ارائهدهندهها، فقط نوبتهایی نمایش داده میشوند که با پزشک(های) خودِ صاحب پرونده گرفته شدهاند:
|
||
|
||
- ارائهدهندهی **پزشک**: نوبتهای بیمار با همان پزشک.
|
||
- ارائهدهندهی **کلینیک** (و منشیِ فعالِ کلینیک): نوبتهای بیمار با پزشکانی که دعوت پذیرفتهشده (`accepted`) در آن کلینیک دارند.
|
||
|
||
مرتبشده بر اساس `starts_at` نزولی. خروجی آرایهی ساده است (بدون صفحهبندی).
|
||
|
||
**Response 200:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "…",
|
||
"starts_at": 1754000000,
|
||
"ends_at": 1754001800,
|
||
"status": "confirmed",
|
||
"doctor_name": "دکتر ژیلا فتحی",
|
||
"service_name": null,
|
||
"price_rials": null,
|
||
"created_at": 1754000000
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
`status` یکی از: `pending`، `confirmed`، `completed`، `cancelled_by_doctor`، `cancelled_by_user`، `no_show`، `expired`. فیلدهای `service_name`/`price_rials` فعلاً همیشه `null` هستند (نوبت خدمت/قیمت مستقل ندارد).
|
||
|
||
**Errors:**
|
||
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_PATIENT_NOT_FOUND` | 404 | Record not found or not owned by caller |
|
||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||
|
||
---
|
||
|
||
### Create Session
|
||
|
||
```
|
||
POST /api/v1/patient/{uuid}/session
|
||
```
|
||
|
||
Creates a new visit session for a patient record.
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{
|
||
"visit_price_rials": 200000,
|
||
"base_insurance_discount_percent": 10,
|
||
"supplementary_discount_percent": 5,
|
||
"insurance_base_id": null,
|
||
"insurance_supplementary_id": null,
|
||
"payment_method": "cash",
|
||
"notes": "...",
|
||
"services": [
|
||
{
|
||
"service_item_uuid": "...",
|
||
"staff_uuid": null,
|
||
"quantity": 2
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Field notes:**
|
||
|
||
- `payment_method`: `cash` | `card` | `insurance` | `online` | `pending`
|
||
- `services`: array of service items to attach; `price_rials` snapshot از ServiceItem؛ `quantity` (پیشفرض ۱) → `line_total_rials = price_rials × quantity`. هر `SessionService` در پاسخ `quantity` و `line_total_rials` دارد.
|
||
- `final_price_rials` (سهم بیمار) به این صورت محاسبه میشود:
|
||
- **ویزیت:** `round(visit_price × (1 - base%) × (1 - supp%))` با درصدهای انتخابشده در فرم.
|
||
- **هر خدمت:** سهم بیمار با قاعدهی پوشش همان بیمهگر برای همان خدمت (`TenantServiceCoverage` از طریق `BillingCalculator`) محاسبه میشود؛ یعنی فقط خدمتی که بیمهی انتخابشده آن را پوشش میدهد تخفیف میگیرد (درصد/فرانشیز/سقف؛ مقدار نبودِ override از قرارداد ارث میبرد). خدمتِ بدون پوشش، کامل بر عهدهی بیمار است.
|
||
- `final_price_rials = سهم بیمار ویزیت + Σ(سهم بیمار هر خدمت)` و `services_total_rials = Σ(price × quantity)` (قیمت کامل خدمات، بدون بیمه).
|
||
- این محاسبه دقیقاً همان منطقِ صورتحساب/مطالبات است؛ پیشنمایش پنل هم همین قاعده را سمت کلاینت آینه میکند.
|
||
|
||
**اتصال خودکار مطالبهی بیمه:** اگر session دارای `insurance_base_id` یا `insurance_supplementary_id` باشد، پس از ثبت بهصورت خودکار صورتحساب ساخته و نهایی میشود و مطالبه(های) بیمه در وضعیت `pending` ایجاد میگردد (پایه/مکمل، فقط برای سهم بیمه > ۰). این مطالبات در صفحهی [مطالبات بیمه](billing.md) قابل پیگیری و ارسالاند. خطا در این مرحله ثبت session را خراب نمیکند (لاگ میشود). برای هر صورتحساب فقط یکبار مطالبه ساخته میشود.
|
||
|
||
**Response 201:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": { ...session object... }
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_PATIENT_NOT_FOUND` | 404 | Record not found or not owned |
|
||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||
|
||
---
|
||
|
||
### Update Session
|
||
|
||
```
|
||
PATCH /api/v1/session/{uuid}
|
||
```
|
||
|
||
Updates mutable fields on a session.
|
||
|
||
**Request body (all optional):**
|
||
|
||
```json
|
||
{
|
||
"notes": "...",
|
||
"payment_method": "card"
|
||
}
|
||
```
|
||
|
||
**Response 200:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": { ...session object... }
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_SESSION_NOT_FOUND` | 404 | Session not found or not owned |
|
||
|
||
---
|
||
|
||
## Auto-Creation on Appointment Confirm
|
||
|
||
When an appointment's status changes to `confirmed` via `PATCH /api/v1/appointment/{uuid}/status`, the system automatically:
|
||
|
||
1. Creates a `PatientRecord` for the appointment's user (if not already existing) under the doctor entity
|
||
2. Creates a blank `PatientSession` linked to the appointment
|
||
3. **اگر نوبت با آدرس کلینیک ثبت شده باشد** (`appointment.address_id` → `DoctorAddress.clinic_id`)، همان دو مرحله برای آن **کلینیک** (`entity_type='clinic'`) هم تکرار میشود. اگر آدرس نوبت کلینیک نداشت ولی دکتر فقط عضو **یک** کلینیک بود، به همان کلینیک اضافه میشود.
|
||
|
||
هر شاخه (doctor / clinic) مستقل و فقط در صورت فعالبودن ویژگی `patient_records` برای همان entity اجرا میشود. duplicate با `findByEntityAndUser` جلوگیری میشود.
|
||
|
||
**انتساب پزشک:** هر `PatientSession` در پاسخ، `doctor_uuid` و `doctor_name` را از روی نوبتِ متناظر برمیگرداند؛ پس در پروندهی کلینیک مشخص است هر مراجعه برای کدام پزشک بوده است.
|
||
|
||
**آدرس نوبت:** هنگام رزرو، `address_id` خودکار از `location_id` همان session برنامهی هفتگی ست میشود (در همهی مسیرهای رزرو). ثبت `location_id` برای هر شیفت فعال در برنامهی هفتگی الزامی است (`POST/PATCH /api/v1/appointment-settings/weekly-schedule`)؛ در غیر این صورت `422`.
|
||
|
||
---
|
||
|
||
## ضمیمههای بیمار (Attachments)
|
||
|
||
فایلهای پیوستِ یک پرونده. همه scope به رکورد و tenant صاحب رکورد.
|
||
|
||
**Permission:** `IS_AUTHENTICATED_FULLY` (doctor/clinic/secretary مالک رکورد)
|
||
|
||
### GET `/api/v1/patient/{uuid}/attachments`
|
||
لیست ضمیمهها. Response: `{ success, data: [{ uuid, name, url, mime, size, created_at }] }`
|
||
|
||
### POST `/api/v1/patient/{uuid}/attachment`
|
||
آپلود فایل بهصورت **raw body** (مثل سایر `/file/upload/...`): بدنه = بایتهای فایل، هدر `Content-Disposition: attachment; filename="..."`. نام نمایشی اختیاری از query `?name=`. فایل زیر `public/uploads/patients/attachments/YYYY-MM/` ذخیره میشود. Response `201`: attachment object.
|
||
|
||
### DELETE `/api/v1/patient/attachment/{uuid}`
|
||
حذف ضمیمه. فقط مالک رکورد؛ در غیر این صورت `404`.
|
||
|
||
### Errors
|
||
| HTTP | Code | Description |
|
||
|------|------|-------------|
|
||
| 404 | `ERR_PATIENT_001` / `ERR_NOT_FOUND_001` | رکورد/ضمیمه یافت نشد یا متعلق به tenant دیگر |
|
||
| 422 | `ERR_VALIDATION_001` | فایل نامعتبر |
|
||
|
||
---
|
||
|
||
## پرونده پزشکی (Medical Records)
|
||
|
||
معاینات/یادداشتهای پزشکیِ یک پرونده. scope به رکورد و tenant صاحب رکورد.
|
||
|
||
**Permission:** `IS_AUTHENTICATED_FULLY` (مالک رکورد)
|
||
|
||
### GET `/api/v1/patient/{uuid}/medical-records`
|
||
لیست (مرتب بر اساس `recorded_at` نزولی). Response: `{ success, data: [{ uuid, title, body, recorded_at, created_at }] }`
|
||
|
||
### POST `/api/v1/patient/{uuid}/medical-record`
|
||
```json
|
||
{ "title": "معاینه اولیه", "body": "شرح (اختیاری)", "recorded_at": 1700000000 }
|
||
```
|
||
`title` الزامی؛ `recorded_at` اختیاری (پیشفرض زمان ثبت). Response `201`.
|
||
|
||
### PATCH `/api/v1/patient/medical-record/{uuid}`
|
||
فیلدهای اختیاری `title` / `body` / `recorded_at`. فقط مالک؛ در غیر این صورت `404`.
|
||
|
||
### DELETE `/api/v1/patient/medical-record/{uuid}`
|
||
حذف. فقط مالک؛ در غیر این صورت `404`.
|
||
|
||
### Errors
|
||
| HTTP | Code | Description |
|
||
|------|------|-------------|
|
||
| 422 | `ERR_VALIDATION_001` | عنوان خالی (`field: title`) |
|
||
| 404 | `ERR_PATIENT_001` / `ERR_NOT_FOUND_001` | رکورد/رکورد پزشکی یافت نشد یا tenant دیگر |
|
||
|
||
---
|
||
|
||
## پیامهای بیمار (Messages)
|
||
|
||
لاگ پیامها/ارتباطات با بیمار (SMS/یادداشت/تماس). scope به رکورد و tenant.
|
||
|
||
**Permission:** `IS_AUTHENTICATED_FULLY` (مالک رکورد)
|
||
|
||
### GET `/api/v1/patient/{uuid}/messages`
|
||
لیست (جدیدترین اول). Response: `{ success, data: [{ uuid, body, channel, created_at }] }`
|
||
|
||
### POST `/api/v1/patient/{uuid}/message`
|
||
```json
|
||
{ "body": "متن پیام", "channel": "sms|note|call|email (اختیاری، پیشفرض sms)" }
|
||
```
|
||
`body` الزامی؛ `channel` نامعتبر → `sms`. Response `201`.
|
||
|
||
### DELETE `/api/v1/patient/message/{uuid}`
|
||
حذف. فقط مالک؛ در غیر این صورت `404`.
|
||
|
||
### Errors
|
||
| HTTP | Code | Description |
|
||
|------|------|-------------|
|
||
| 422 | `ERR_VALIDATION_001` | متن خالی (`field: body`) |
|
||
| 404 | `ERR_PATIENT_001` / `ERR_NOT_FOUND_001` | رکورد/پیام یافت نشد یا tenant دیگر |
|
||
|
||
---
|
||
|
||
## مالی بیمار (Financials: پرداخت / تراکنش / کیفپول)
|
||
|
||
مالیِ **کاربرِ صاحبِ رکورد** (بیمار)، gate شده به مالکیت رکورد. اندپوینتهای عمومی `wallet/*` و `my/payments` به `#[CurrentUser]` (پولِ خودِ درخواستکننده) بستهاند؛ این اندپوینتها مالیِ بیمار را برای دکتر/منشیِ صاحب پرونده برمیگردانند.
|
||
|
||
**Permission:** `IS_AUTHENTICATED_FULLY` (مالک رکورد)
|
||
|
||
### GET `/api/v1/patient/{uuid}/payments`
|
||
لیست پرداختهای درگاهیِ بیمار (paginated). Query: `page`, `limit` (≤100)، `status` (اختیاری: `pending|success|failed|canceled|refunded`).
|
||
Response: `{ success, data: [{ uuid, order_id, amount_rials, status, gateway, type, reference_id, appointment_uuid, created_at }], meta: { totalRecords, totalPages, currentPage } }`
|
||
|
||
### GET `/api/v1/patient/{uuid}/wallet`
|
||
موجودی + ۱۰ تراکنش اخیر (تب کیفپول). `balance_rials` = مجموع credit − debit.
|
||
Response: `{ success, data: { balance_rials, recent_transactions: [{ uuid, amount_rials, type, description, balance_after, created_at }] } }`
|
||
|
||
### POST `/api/v1/patient/{uuid}/wallet/charge`
|
||
شارژ دستی کیفپول (مثلاً بیعانهٔ حضوری). یک تراکنش `credit` برای کاربرِ صاحب رکورد میسازد.
|
||
```json
|
||
{ "amount_rials": 300000, "description": "بیعانه نوبت (اختیاری، پیشفرض «شارژ کیف پول»)" }
|
||
```
|
||
`amount_rials` باید > 0 باشد وگرنه `422`. Response `201`: `{ success, data: { transaction, balance_rials } }`
|
||
|
||
### GET `/api/v1/patient/{uuid}/wallet/transactions`
|
||
دفترِ کاملِ تراکنشهای کیفپول (paginated). Query: `page`, `limit` (≤100).
|
||
Response: `{ success, data: [{ uuid, amount_rials, type, description, balance_after, created_at }], meta: { totalRecords, totalPages, currentPage } }`
|
||
|
||
### Errors
|
||
| HTTP | Code | Description |
|
||
|------|------|-------------|
|
||
| 404 | `ERR_PATIENT_001` | رکورد یافت نشد یا متعلق به مالک دیگر |
|
||
|
||
---
|
||
|
||
## کال سنتر بیمار (Call Center)
|
||
|
||
لاگ تماسهای تلفنی با بیمار (تب «کال سنتر»). scope به رکورد و مالک.
|
||
|
||
**Permission:** `IS_AUTHENTICATED_FULLY` (مالک رکورد)
|
||
|
||
### GET `/api/v1/patient/{uuid}/calls`
|
||
لیست (جدیدترین بر اساس `called_at`). Query: `outcome` (اختیاری: `success|missed`).
|
||
Response: `{ success, data: [{ uuid, subject, summary, outcome, called_at, personnel, created_at }] }`
|
||
|
||
### POST `/api/v1/patient/{uuid}/call`
|
||
```json
|
||
{ "subject": "پیگیری نوبت", "summary": "اختیاری", "outcome": "success|missed (پیشفرض success)", "called_at": 1731000000, "personnel": "نام ثبتکننده (اختیاری)" }
|
||
```
|
||
`subject` الزامی؛ `outcome` نامعتبر → `success`؛ `called_at` غایب → اکنون. Response `201`.
|
||
|
||
### DELETE `/api/v1/patient/call/{uuid}`
|
||
حذف. فقط مالک؛ در غیر این صورت `404`.
|
||
|
||
### Errors
|
||
| HTTP | Code | Description |
|
||
|------|------|-------------|
|
||
| 422 | `ERR_VALIDATION_001` | موضوع خالی (`field: subject`) |
|
||
| 404 | `ERR_PATIENT_001` | رکورد/تماس یافت نشد یا مالک دیگر |
|