11 KiB
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:
{
"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).
سه حالت پشتیبانی میشود:
- کاربر ثبتنامکرده با uuid:
user_uuidارسال شود. - کاربر ثبتنامکرده با موبایل:
mobileارسال شود (کاربر موجود پیدا میشود). - بیمار جدید بدون ثبتنام:
mobile+nameارسال شود؛ اگر کاربری با آن موبایل نباشد،Userجدید (نقشROLE_USER، بدون رمز عبور) ساخته میشود سپس پرونده.
Request body:
{
"user_uuid": "string (اختیاری)",
"mobile": "09xxxxxxxxx (اختیاری — برای جستجو یا ساخت بیمار جدید)",
"name": "string (الزامی فقط هنگام ساخت بیمار جدید)",
"national_code": "string (اختیاری، ۱۰ رقم)"
}
- اگر
user_uuidوmobileهر دو خالی باشند → خطا. national_codeفقط وقتی روی کاربر ست میشود که کاربر کد ملی نداشته باشد.- موبایل تکراری duplicate نمیسازد؛ همان کاربر استفاده میشود.
- یکتایی کد ملی: اگر
national_codeارسالی قبلاً به پروفایل کاربر دیگری تعلق داشته باشد →409با کدERR_PROFILE_001(field: national_code). یک کد ملی = یک بیمار در کل سیستم (همراستا با قید یکتایprofiles.national_code).
Response 201:
{
"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:
{
"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:
{
"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:
{
"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|pendingservices: array of service items to attach;price_rialssnapshot از 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 ایجاد میگردد (پایه/مکمل، فقط برای سهم بیمه > ۰). این مطالبات در صفحهی مطالبات بیمه قابل پیگیری و ارسالاند. خطا در این مرحله ثبت session را خراب نمیکند (لاگ میشود). برای هر صورتحساب فقط یکبار مطالبه ساخته میشود.
Response 201:
{
"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):
{
"notes": "...",
"payment_method": "card"
}
Response 200:
{
"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:
- Creates a
PatientRecordfor the appointment's user (if not already existing) under the doctor entity - Creates a blank
PatientSessionlinked to the appointment - اگر نوبت با آدرس کلینیک ثبت شده باشد (
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.