Files
clinicpro/docs/api/clinic-services.md
T
hamed 42d9ad26c5 Add tests and implementation for ServiceDetailPage and PriceInput components
- Implement PriceInput component tests to validate Persian and Arabic numeral handling, input formatting, and controlled behavior.
- Create ServiceDetailPage component with detailed service information, including pricing, insurance coverage, and editing capabilities.
- Add API tests for service item detail retrieval and coverage synchronization with insurance contracts.
- Ensure proper error handling and user feedback for service item retrieval and coverage management.
2026-07-18 12:10:49 +03:30

10 KiB

Clinic Services API

مدیریت بخش‌ها و سرویس‌های کلینیک/مطب.

نیاز به پنل: Basic یا بالاتر (ERR_SUBSCRIPTION_REQUIRED اگر نداشت)


GET /api/v1/service-sections

لیست بخش‌های سرویس entity جاری.

Permission: IS_AUTHENTICATED_FULLY + پنل Basic+

Response 200:

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "entity_type": "clinic",
      "entity_id": 5,
      "name": "آزمایشگاه",
      "active": true,
      "items_count": 10,
      "created_at": 1718000000,
      "updated_at": 1718000000
    }
  ]
}

items_count تعداد سرویس‌های همان بخش است (فقط در این endpoint لیستی برگردانده می‌شود).


POST /api/v1/service-section

ایجاد بخش جدید.

Permission: IS_AUTHENTICATED_FULLY + پنل Basic+

Request Body:

{ "name": "رادیولوژی" }

Response 201: ServiceSection object

Errors:

Code HTTP توضیح
ERR_SUBSCRIPTION_REQUIRED 403 نیاز به پنل Basic+
ERR_VALIDATION_001 422 name خالی است

PATCH /api/v1/service-section/{uuid}

ویرایش بخش.

Permission: owner یا ROLE_ADMIN

{ "name": "رادیولوژی دیجیتال", "active": true }

DELETE /api/v1/service-section/{uuid}

حذف بخش (cascade — همه ServiceItem های آن حذف می‌شوند).

Permission: owner


GET /api/v1/service-items

همه‌ی سرویس‌های owner در همه‌ی بخش‌ها (برای انتخاب/جستجوی سراسری در فرم ثبت/ویرایش مراجعه). پاسخ مثل لیست هر بخش (آرایه‌ی ServiceItem::toArray)، مرتب بر نام.

GET /api/v1/service-items/{sectionUuid}

لیست سرویس‌های یک بخش.

Response 200:

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "section_uuid": "...",
      "staff_uuid": "...",
      "staff_name": "علی محمدی",
      "staff": { "uuid": "...", "full_name": "علی محمدی" },
      "staff_members": [
        { "uuid": "...", "full_name": "علی محمدی" },
        { "uuid": "...", "full_name": "سحر رحمانی" }
      ],
      "name": "رادیوگرافی مستقیم",
      "price_rials": 500000,
      "active": true,
      "insurance_covered": false,
      "insurance_price_rials": null,
      "duration_minutes": 50,
      "bookable": true,
      "created_at": 1718000000,
      "updated_at": 1718000000
    }
  ]
}

GET /api/v1/service-item/{uuid}

یک سرویس مشخص — پشتیبان صفحه‌ی اختصاصی «جزئیات سرویس» (/admin/clinic-services/{uuid}) که باید با refresh مستقیم هم کار کند، بنابراین فیلترکردن سمت کلاینت از فهرست کامل کافی نبود.

Permission: IS_AUTHENTICATED_FULLY + پنل Basic+ · فقط سرویس‌های همان مطب/کلینیک (ServiceItem→section→entity_type/entity_id).

Response 200: یک ServiceItem object (ساختار یکسان با آیتم‌های فهرست، شامل section_uuid، section_name، staff_members، created_at و updated_at):

{
  "success": true,
  "data": {
    "uuid": "...",
    "section_uuid": "...",
    "section_name": "تزریقات",
    "name": "سرم ۵۰۰cc",
    "price_rials": 850000,
    "active": true,
    "insurance_covered": true,
    "insurance_price_rials": null,
    "duration_minutes": 30,
    "bookable": true,
    "staff": { "uuid": "...", "full_name": "مریم امینی" },
    "staff_members": [{ "uuid": "...", "full_name": "مریم امینی" }],
    "created_at": 1718000000,
    "updated_at": 1718000000
  }
}

خطاها: 404 ERR_SERVICE_NOT_FOUND — هم برای uuid ناموجود و هم برای سرویس متعلق به tenant دیگر (وجود سرویس نباید لو برود) · 401 بدون احراز هویت.

section_name تازه به ServiceItem::toArray() اضافه شده و در همه‌ی پاسخ‌های این فایل هست، نه فقط این endpoint.


POST /api/v1/service-item

ایجاد سرویس جدید.

Permission: IS_AUTHENTICATED_FULLY + پنل Basic+

Request Body:

{
  "section_uuid": "...",
  "name": "رادیوگرافی مستقیم",
  "price_rials": 500000,
  "staff_uuid": "...",
  "insurance_covered": true,
  "insurance_price_rials": 200000,
  "duration_minutes": 50,
  "bookable": true
}
فیلد نوع الزامی
section_uuid UUID
name string
price_rials integer (پیش‌فرض 0) — «قیمت پایه»
staff_uuids UUID[] — پرسنل مسئول (چند نفر). ترجیح داده می‌شود
staff_uuid UUID — legacy تک‌پرسنل (اگر staff_uuids نباشد استفاده می‌شود)
insurance_covered boolean (پیش‌فرض false) — deprecated برای نوشتن. پنل ادمین دیگر این فیلد را نمی‌فرستد؛ مقدارش به‌صورت خودکار از ردیف‌های پوشش بیمه همگام می‌شود (به insurance.md نگاه کن). endpoint هنوز آن را می‌پذیرد تا کلاینت‌های قدیمی نشکنند، ولی ذخیره‌ی پوشش بعداً آن را بازنویسی می‌کند
insurance_price_rials integer|null deprecated. سهم تقریبی بیمار؛ از فرم سرویس حذف شد. محاسبه‌ی دقیق سهم بیمار از TenantServiceCoverage انجام می‌شود
duration_minutes integer|null — «زمان متوسط» انجام خدمت به دقیقه (""/null = بدون مقدار)
bookable boolean (پیش‌فرض false) — «نمایش در نوبت‌دهی». فقط سرویس‌های bookable=true در حالت نوبت‌دهی سرویسی قابل‌انتخاب‌اند

bookable در PATCH /api/v1/service-item/{uuid} هم به همین شکل پذیرفته می‌شود.

Response 201: ServiceItem object (شامل insurance_covered و insurance_price_rials)

قیمت واحد: هنگام ساخت سرویس، یک تعرفه برای سال جاری با همان price_rials به‌صورت خودکار ثبت می‌شود. قیمت سرویس = تعرفه‌ی سال جاری است و همه‌جا (صورتحساب، مراجعه، مطالبات) از همین قیمت استفاده می‌شود.


PATCH /api/v1/service-item/{uuid}

ویرایش سرویس.

{
  "name": "رادیوگرافی دیجیتال",
  "price_rials": 600000,
  "staff_uuid": null,
  "active": false,
  "insurance_covered": true,
  "insurance_price_rials": 250000,
  "duration_minutes": 30
}

فیلد duration_minutes (زمان متوسط، دقیقه) در پاسخِ toArray و در ساخت/ویرایش پشتیبانی می‌شود؛ ""/null آن را پاک می‌کند.

پرسنل چندنفره: یک سرویس می‌تواند چند پرسنل داشته باشد. staff_uuids (آرایه) ترجیح داده می‌شود؛ در نبود آن، staff_uuid تک‌نفره به‌صورت backward-compatible پذیرفته می‌شود. پاسخ همیشه staff_members[] (کامل) و staff/staff_uuid/staff_name (نفر اول، برای سازگاری) را برمی‌گرداند. هر پرسنل باید متعلق به همان tenant (entity_type/entity_id) باشد؛ در غیر این صورت → 422 ERR_VALIDATION_001 (field: staff_uuids). همین قید روی POST /service-item نیز اعمال می‌شود.


DELETE /api/v1/service-item/{uuid}

حذف سرویس.

اگر سرویس در پرونده بیماری استفاده شده باشد، خطا برمی‌گرداند:

{
  "success": false,
  "errors": [{ "code": "ERR_SERVICE_ITEM_IN_USE", "message": "این سرویس در پرونده بیمار ثبت شده است" }]
}

Errors:

Code HTTP توضیح
ERR_SUBSCRIPTION_REQUIRED 403 نیاز به پنل Basic+
ERR_SERVICE_NOT_FOUND 404 سرویس یافت نشد
ERR_SERVICE_ITEM_IN_USE 409 سرویس در پرونده بیمار استفاده شده

تعرفه‌ی نسخه‌دار سالانه (Tariff) — فاز ۳ سیستم صورتحساب

هر خدمت می‌تواند برای هر سال شمسی یک تعرفه داشته باشد. اگر تعرفه‌ی سالی ثبت نشود، به price_rials خود خدمت fallback می‌شود (TariffService::resolvePrice). سال جاری شمسی سمت سرور با IntlDateFormatter (تقویم persian) محاسبه می‌شود.

GET /api/v1/service-items/{uuid}/tariffs

لیست تعرفه‌های یک خدمت + قیمت پیش‌فرض + سال جاری.

Permission: IS_AUTHENTICATED_FULLY (مالک خدمت)

{
  "success": true,
  "data": {
    "current_year": 1405,
    "default_price_rials": 500000,
    "data": [
      { "uuid": "…", "service_item_id": 12, "year": 1405, "price_rials": 600000, "is_active": true },
      { "uuid": "…", "service_item_id": 12, "year": 1404, "price_rials": 500000, "is_active": true }
    ]
  }
}

PUT /api/v1/service-items/{uuid}/tariffs/{year}

ثبت/به‌روزرسانی تعرفه‌ی یک سال (upsert). year بین ۱۳۹۰ تا ۱۵۰۰.

Body:

{ "price_rials": 600000 }

Response 200: { success, data: { …tariff } }

اگر year برابر سال جاری باشد، ServiceItem.price_rials هم با همین مقدار همگام می‌شود (قیمت واحد). تعرفه‌ی سال‌های دیگر فقط برای محاسبه‌ی صورتحساب همان سال (TariffService::resolvePrice) به‌کار می‌رود و قیمت پایه‌ی سرویس را تغییر نمی‌دهد. هم‌چنین PATCH /service-item/{uuid} با تغییر price_rials، تعرفه‌ی سال جاری را upsert می‌کند.

Errors:

Code HTTP توضیح
ERR_SERVICE_NOT_FOUND 404 سرویس یافت نشد
ERR_VALIDATION_001 422 سال نامعتبر