Files
clinicpro/docs/api/patient.md
T
hamedandClaude Opus 4.8 665a210ef8 feat(patients): phase D — call-center tab (patient call log)
Add a record-scoped PatientCall entity (subject, summary, outcome
success/missed, called_at, personnel) with its repository and three
owner-gated endpoints on PatientController:

  GET    /patient/{uuid}/calls   — call log, newest first, optional ?outcome
  POST   /patient/{uuid}/call    — log a call (subject required)
  DELETE /patient/call/{uuid}    — delete an entry

Wire the previously-placeholder "کال سنتر" tab as a CallCenterTab: a register
form (date/time/subject/summary + success/missed toggle, personnel taken from
the logged-in user) beside a filterable call history (all / success / missed).
With this every patient-detail tab is now backed by a real endpoint, so the
generic Placeholder is no longer reachable. PatientCallTest covers create/list/
delete, the outcome filter, the invalid-outcome fallback, and ownership scoping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:01:16 +03:30

616 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (1050) |
| `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 }] } }`
### 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` | رکورد/تماس یافت نشد یا مالک دیگر |