# 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 (اختیاری، ۱۰ رقم)" } ``` - اگر `user_uuid` و `mobile` هر دو خالی باشند → خطا. - `national_code` فقط وقتی روی کاربر ست می‌شود که کاربر کد ملی نداشته باشد. - موبایل تکراری duplicate نمی‌سازد؛ همان کاربر استفاده می‌شود. **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_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": "محمد محمدی", "national_code": "0012345678", "gender": "male", "date_of_birth": 700000000, "blood_type": "O+", "marital_status": "single", "job": "...", "address": "...", "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 | --- ### List Patient Sessions ``` GET /api/v1/patient/{uuid}/sessions ``` Returns paginated sessions for a patient record. **Query params:** `page`, `limit` (same as list) **Response 200:** ```json { "success": true, "data": [ { "uuid": "...", "record_uuid": "...", "appointment_uuid": null, "insurance_base_id": null, "insurance_supplementary_id": null, "visit_price_rials": 200000, "base_insurance_discount_percent": "10.00", "supplementary_discount_percent": "5.00", "services_total_rials": 50000, "final_price_rials": 230000, "payment_method": "cash", "notes": "...", "created_at": 1718375000, "updated_at": 1718375000 } ], "meta": { "totalRecords": 8, "totalPages": 1, "currentPage": 1 } } ``` **Errors:** | Code | HTTP | Description | |------|------|-------------| | `ERR_PATIENT_NOT_FOUND` | 404 | Record not found or not owned by caller | | `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature | --- ### Create Session ``` POST /api/v1/patient/{uuid}/session ``` Creates a new visit session for a patient record. **Request body:** ```json { "visit_price_rials": 200000, "base_insurance_discount_percent": 10, "supplementary_discount_percent": 5, "insurance_base_id": null, "insurance_supplementary_id": null, "payment_method": "cash", "notes": "...", "services": [ { "service_item_uuid": "...", "staff_uuid": null, "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` is computed: `(visit_price × (1 - base%) × (1 - supp%)) + services_total` که `services_total = Σ(price × quantity)` **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`.