Files
clinicpro/docs/api/patient.md
T
hamedandClaude Fable 5 7da7d968b9 feat(patient): session edit + payment PATCH/DELETE + audit-log endpoints
updateSession now accepts services/consumables/visit_price/insurance and calls
updateSessionServices; discount paths pass the actor for audit. Add
PATCH/DELETE /session/{uuid}/payments/{paymentUuid} and GET
/session/{uuid}/audit-log (owner-scoped). Inject the payment + audit repos.
Verified end-to-end (edit visit price, payment edit-exceeds guard, delete +
recompute, audit trail). Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:14:49 +03:30

40 KiB
Raw Blame History

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, phone or national code
tags string Comma-separated tenant-tag uuids; matches records having any of them
gender string Patient UserProfile.gender (e.g. male/female)
insurance_id int Patient's basic insurance id (UserProfile.basic_insurance_id)
admitted_from / admitted_to int Record creation (تاریخ پذیرش) unix-seconds range
service_status string pending (has an unpaid session) or completed (has sessions, none unpaid)
has_debt bool 1 → only records with an unpaid session (payment_method='pending')

«بدهی» و «وضعیت سرویس» بر پایه‌ی وجود مراجعه‌ی پرداخت‌نشده تعریف شده‌اند (مدل بدهی مستقل ندارد). فیلترها روی هم AND می‌شوند و در count هم اعمال می‌گردند.

Response 200:

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "entity_type": "doctor",
      "entity_id": 5,
      "user_uuid": "...",
      "user_name": "علی رضایی",
      "user_mobile": "09123456789",
      "user_national_code": "0012345675",
      "record_number": "1024",
      "tags": [],
      "created_by_type": "doctor",
      "created_at": 1718375000
    }
  ],
  "meta": {
    "totalRecords": 42,
    "totalPages": 3,
    "currentPage": 1
  }
}

user_national_code منبعِ حقیقتش جدول profiles است (نه users). اگر روی خودِ کاربر خالی باشد، از پروفایل پر می‌شود؛ اگر هیچ‌کدام نداشته باشند null است.

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:

{
  "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:

{
  "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": "محمد محمدی",
      "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:

{
  "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, و filter = active (پیش‌فرض — آرشیوها مخفی) | all | archived. مقادیر نامعتبر به active برمی‌گردند. هر session کلیدهای archived (bool) و archived_at (unix|null) را هم دارد.

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",
      "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:

{
  "success": true,
  "data": [
    {
      "uuid": "…",
      "starts_at": 1754000000,
      "ends_at": 1754001800,
      "status": "confirmed",
      "version": 1,
      "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 هستند (نوبت خدمت/قیمت مستقل ندارد). version نسخهٔ خوش‌بینانهٔ (optimistic-lock) نوبت است و برای فراخوانی PATCH /api/v1/appointment/{uuid}/status لازم است.

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": "...",
  "session_at": 1760000000,
  "inventory_package_uuid": null,
  "services": [
    {
      "service_item_uuid": "...",
      "staff_uuid": null,
      "quantity": 2
    }
  ],
  "consumables": [
    {
      "inventory_item_uuid": "...",
      "quantity": 2
    }
  ]
}

Field notes:

  • visit_price_rials: هزینه ویزیت (ریال). به‌طور پیش‌فرض اختیاری (پیش‌فرض 0)؛ اگر فلگ require_visit_price در insurance-pricing برای tenant فعال باشد، مقدار > 0 الزامی است.
  • payment_method: cash | card | insurance | online | pending
  • session_at (اختیاری): زمان پذیرش (unix)؛ اگر نیاید null می‌ماند و زمان ثبت (created_at) مبنا است.
  • inventory_package_uuid (اختیاری): مرجع پکیج مصرفی (inventory)؛ فقط پکیج متعلق به همان tenant پذیرفته می‌شود، وگرنه بی‌صدا نادیده گرفته می‌شود. روی قیمت اثری ندارد (فقط مرجع).
  • consumables (اختیاری): کالاهای مصرفی از انبار (inventory). price_rials snapshot از InventoryItem.price؛ quantity (پیش‌فرض ۱، حداقل ۱). کالاها پوشش بیمه ندارند و مبلغ کاملشان به final_price_rials (سهم بیمار) اضافه می‌شود. آیتم ناموجود یا متعلق به tenant دیگر بی‌صدا رد می‌شود (هم‌رفتار با services). پاسخ شامل consumables[] (با line_total_rials) و consumables_total_rials است.
  • 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) (قیمت کامل خدمات، بدون بیمه). کالاهای مصرفی در consumables_total_rials جدا گزارش می‌شوند.
    • این محاسبه دقیقاً همان منطقِ صورتحساب/مطالبات است؛ پیش‌نمایش پنل هم همین قاعده را سمت کلاینت آینه می‌کند.

اتصال خودکار مطالبه‌ی بیمه: اگر 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
ERR_VALIDATION_001 422 فلگ require_visit_price فعال است ولی visit_price_rials <= 0 (field: visit_price_rials)

Update Session

PATCH /api/v1/session/{uuid}

Updates mutable fields on a session.

Request body (all optional):

{
  "notes": "...",
  "payment_method": "card",
  "paid_at": 1770000000,
  "discount_type": "percent",
  "discount_value": 25
}
  • payment_method: "wallet" روی مراجعه‌ی تسویه‌نشده، کل مبلغ نهایی را از کیف پول بیمار کسر می‌کند (موجودی ناکافی → 422 ERR_WALLET_INSUFFICIENT).
  • تخفیف تسویه (دستی): discount_type = percent (۰..۱۰۰) یا fixed (ریال، حداکثر برابر مبلغ نهایی) یا null (حذف تخفیف). مبلغ محاسبه‌شده در discount_rials برمی‌گردد. تخفیف نمی‌تواند از «مبلغ نهایی منهای پرداخت‌های ثبت‌شده» بیشتر شود. تخفیفی که مانده را صفر کند مراجعه را تسویه‌شده می‌کند (is_paid, paid_at).
  • تخفیف بر اساس قانون: discount_rule_uuid (رشته) → قانون تخفیف (owner-scoped) اعمال می‌شود؛ مقدار ریالی از خود قانون توسط موتور محاسبه می‌گردد (نوع/مبنا بر اساس قانون). ''/null → حذف تخفیف. اولویت بر discount_type دستی. قانونِ نامعتبر → 404. منبع اعمال‌شده در پاسخ به‌صورت applied_discount_rule_id و applied_discount_rule_label (audit) برمی‌گردد. قوانین قابل‌اعمال از GET /api/v1/session/{uuid}/discount-suggestions (نگاه کنید به discount.md).
  • paid_at: unix timestamp زمان تسویه.
  • آرشیو: archived (bool) → آرشیو نرم مراجعه؛ true آن را از لیست پیش‌فرض (filter=active) مخفی می‌کند و archived_at را ست می‌کند، false بازمی‌گرداند. سابقه (فاکتور/پرداخت‌ها) حذف نمی‌شود.

Response 200:

{
  "success": true,
  "data": { ...session object... }
}

Errors:

Code HTTP Description
ERR_SESSION_NOT_FOUND 404 Session not found or not owned
ERR_SESSION_DISCOUNT_INVALID 422 نوع/مقدار تخفیف نامعتبر یا بیش از سقف
ERR_WALLET_INSUFFICIENT 422 موجودی کیف پول کافی نیست (روش wallet)

Add Session Payment (تسویه چندتکه)

POST /api/v1/session/{uuid}/payments

ثبت یک پرداخت جزئی روی مراجعه. مجموع پرداخت‌ها + تخفیف که به مبلغ نهایی برسد، مراجعه تسویه‌شده می‌شود (is_paid=true، payment_method = روش آخرین پرداخت، paid_at ست می‌شود).

Request body:

{
  "method": "wallet | pos | cash | card",
  "amount_rials": 200000,
  "paid_at": 1770000000
}
  • method: "wallet" همان مبلغ را از کیف پول بیمار کسر می‌کند (تراکنش debit با reference: "session:{uuid}").
  • paid_at اختیاری است (پیش‌فرض: اکنون).

Response 201: session object با فیلدهای صورتحساب:

{
  "success": true,
  "data": {
    "...": "...session fields...",
    "discount_type": "fixed",
    "discount_value": 100000,
    "discount_rials": 100000,
    "paid_total_rials": 300000,
    "patient_debt_rials": 0,
    "paid_at": 1770000000,
    "payments": [
      { "uuid": "...", "method": "cash", "amount_rials": 200000, "paid_at": 1770000000, "created_by_name": "...", "created_at": 1770000000 }
    ]
  }
}

Errors:

Code HTTP Description
ERR_SESSION_NOT_FOUND 404 Session not found or not owned
ERR_SESSION_PAYMENT_INVALID 422 روش نامعتبر یا مبلغ ≤ ۰
ERR_SESSION_PAYMENT_EXCEEDS 422 مبلغ از مانده بدهی بیشتر است
ERR_WALLET_INSUFFICIENT 422 موجودی کیف پول کافی نیست (روش wallet)

Session list debt: در GET /api/v1/patient/{uuid}/sessions، فیلد patient_debt_rials = سهم بیمار (از فاکتور در صورت وجود) منهای discount_rials و paid_total_rials.


Edit Session Payment

PATCH  /api/v1/session/{uuid}/payments/{paymentUuid}
DELETE /api/v1/session/{uuid}/payments/{paymentUuid}

ویرایش/حذف یک پرداخت ثبت‌شده. پس از تغییر، فیلدهای کش‌شده‌ی تسویه (payment_method، paid_at، is_paid) و paid_total_rials/patient_debt_rials بازمحاسبه می‌شوند. هر عملیات در Audit Log ثبت می‌شود.

PATCH body (همه اختیاری): { "method": "pos|cash|card", "amount_rials": 300000, "paid_at": 1770000000 }

  • پرداخت wallet قابل ویرایش/حذف نیست (422 — جبران تراکنش کیف پول پشتیبانی نمی‌شود).
  • مجموع پرداخت‌ها پس از ویرایش نباید از «مبلغ نهایی منهای تخفیف» بیشتر شود.

Response 200: session object با فیلدهای صورتحساب (مثل بالا).

Code HTTP Description
ERR_SESSION_NOT_FOUND 404 Session یافت نشد یا متعلق به owner نیست
ERR_SESSION_PAYMENT_INVALID 404/422 پرداخت یافت نشد / روش نامعتبر / پرداخت wallet
ERR_SESSION_PAYMENT_EXCEEDS 422 مبلغ از مانده بیشتر است

Session Audit Log

GET /api/v1/session/{uuid}/audit-log

تاریخچه‌ی کامل تغییرات مالی/خدماتی مراجعه (جدید → قدیم). هر رکورد:

{
  "success": true,
  "data": [
    { "field": "visit_price_rials", "operation": "update", "old_value": "1000000", "new_value": "900000", "actor_name": "دکتر ...", "note": null, "created_at": 1770000000 },
    { "field": "payment", "operation": "delete", "old_value": "200000", "new_value": null, "actor_name": "منشی ...", "note": "حذف پرداخت", "created_at": 1770000100 }
  ]
}

field یکی از: visit_price_rials | services | consumables | services_total_rials | final_price_rials | payment | discount. operation: create | update | delete. مقادیر پول ریال؛ created_at unix.

ویرایش سرویس‌ها/کالاها/قیمت: PATCH /api/v1/session/{uuid} علاوه بر فیلدهای قبلی، اکنون services[]، consumables[]، visit_price_rials، insurance_base_id/insurance_supplementary_id، base_insurance_discount_percent/supplementary_discount_percent را هم می‌پذیرد (بدنه مثل ایجاد سرویس). مجموع‌ها بازمحاسبه و هر فیلد تغییرکرده در audit-log ثبت می‌شود.


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_idDoctorAddress.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

{ "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

{ "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 دیگر

توجه: پنل ادمین دیگر تب «پیام‌ها» را نشان نمی‌دهد؛ جای آن «یادداشت‌ها» آمده است. این اندپوینت‌ها باقی می‌مانند ولی توسط پنل مصرف نمی‌شوند.


یادداشت‌های بیمار (Notes)

یادداشت‌های شخصیِ پرسنل روی پرونده، پین‌شدنی. مشترک بین همه‌ی کارکنانِ صاحبِ پرونده؛ نام سازنده هنگام ثبت ذخیره می‌شود (پس از حذف کاربر هم باقی می‌ماند). scope به رکورد و tenant.

Permission: IS_AUTHENTICATED_FULLY (مالک رکورد)

GET /api/v1/patient/{uuid}/notes

لیست، پین‌شده‌ها اول، سپس جدیدترین. Response: { success, data: [{ uuid, body, pinned, author, created_at, updated_at }] }

POST /api/v1/patient/{uuid}/note

{ "body": "متن یادداشت", "pinned": false }

body الزامی (trim)؛ pinned اختیاری (پیش‌فرض false). author/سازنده سمت سرور از کاربر جاری (real_name یا موبایل) پر می‌شود. Response 201.

PATCH /api/v1/patient/note/{uuid}

{ "body": "متن جدید", "pinned": true }

هر دو فیلد اختیاری (partial). با ارسال pinned تنها → toggle پین بدون تغییر متن. body خالی → 422. updated_at ست می‌شود. فقط مالک.

DELETE /api/v1/patient/note/{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 } }

هر تراکنش برای شفافیت این فیلدها را دارد: type (credit/debit)، payment_method (card/pos/cash/gateway/wallet یا null)، description (دلیل)، reference (مرجعِ ماشینی مثل session:{uuid}created_by_name (کاربرِ ثبت‌کننده)، status (confirmedbalance_after، created_at.

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_by_name, payment_method, reference, status, created_at }] } }

POST /api/v1/patient/{uuid}/wallet/charge

شارژ دستی کیف‌پول (مثلاً بیعانهٔ حضوری). یک تراکنش credit برای کاربرِ صاحب رکورد می‌سازد؛ کاربرِ درخواست‌کننده به‌عنوان created_by ثبت می‌شود.

{ "amount_rials": 300000, "description": "بیعانه نوبت (اختیاری)", "payment_method": "card", "reference": "(اختیاری)" }

amount_rials باید > 0 باشد وگرنه 422. payment_method ناشناخته نادیده گرفته می‌شود (null). Response 201: { success, data: { transaction, balance_rials } }

POST /api/v1/patient/{uuid}/wallet/withdraw

برداشت دستی از کیف‌پول (مثلاً عودت وجه حضوری). یک تراکنش debit با ثبتِ کاربرِ عامل و روش پرداخت می‌سازد.

{ "amount_rials": 200000, "description": "عودت (اختیاری)", "payment_method": "cash" }

amount_rials باید > 0 باشد وگرنه 422. اگر مبلغ از موجودی فعلی بیشتر باشد 422 با کد ERR_WALLET_INSUFFICIENT. Response 201: { success, data: { transaction, balance_rials } }

PATCH /api/v1/session/{uuid} — پرداخت مراجعه از کیف پول

با {"payment_method": "wallet"} سهمِ نهاییِ بیمار (final_price_rials) از کیف پول کسر می‌شود: یک تراکنشِ debit با payment_method=wallet، reference=session:{uuid} و دلیلِ «پرداخت سرویس: …» ثبت می‌گردد. فقط وقتی مراجعه هنوز تسویه نشده و مبلغ > 0 باشد. موجودیِ ناکافی → 422 ERR_WALLET_INSUFFICIENT (مراجعه تسویه نمی‌شود).

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 رکورد یافت نشد یا متعلق به مالک دیگر
422 ERR_VALIDATION_001 مبلغ شارژ/برداشت ≤ 0
422 ERR_WALLET_INSUFFICIENT مبلغ برداشت از موجودی کیف‌پول بیشتر است

کال سنتر بیمار (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

{ "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 رکورد/تماس یافت نشد یا مالک دیگر