Files
clinicpro/docs/api/admin.md
T
hamed 8d2b0d908a feat: Add online share functionality for secretaries
- Introduced `online_share_enabled` and `online_share_percent` fields in the `doctor_secretaries` table to manage secretary shares from online appointments.
- Added `bank_account` field in the `profiles` table to store user-level IBANs for settlements.
- Created `secretary_earnings` table to track earnings per secretary from online appointments, including a foreign key relationship with `financial_breakdowns`.
- Implemented `SecretaryEarning` entity and repository for managing secretary earnings.
- Developed `SecretaryShareResolver` service to determine which secretaries earn from online payments.
- Added `UserIbanResolver` service to handle user IBAN retrieval and management.
- Created `HasIbansTrait` for entities to manage IBANs in a JSON format.
- Implemented tests for secretary earnings and API endpoints for managing secretary shares and IBANs.
2026-07-25 18:34:18 +03:30

50 KiB
Raw Blame History

Admin API

Prefix: /api/v1/admin
Permission: ALL endpoints in this file require ROLE_ADMIN
Headers: Authorization: Bearer <admin_jwt_token>


Dashboard

GET /api/v1/admin/dashboard/stats

Get key performance indicators (KPIs) for the dashboard.

Permission: ROLE_ADMIN

Response 200

{
  "success": true,
  "data": {
    "total_users": 1200,
    "active_doctors": 85,
    "total_doctors": 92,
    "total_clinics": 34,
    "today_appointments": 47,
    "total_appointments": 8540,
    "today_payments_count": 30,
    "today_payments_amount": 15000000,
    "total_payments_amount": 425000000,
    "pending_comments": 12,
    "pending_settlements": 5,
    "this_month_revenue": 52000000,
    "this_month_appointments": 620
  }
}

GET /api/v1/admin/dashboard/charts

Get chart data for the last 30 days.

Permission: ROLE_ADMIN

Response 200

{
  "success": true,
  "data": {
    "appointments_30d": [
      { "date": "2024-06-01", "count": 42 }
    ],
    "revenue_30d": [
      { "date": "2024-06-01", "amount_rials": 21000000 }
    ],
    "appointment_status": {
      "confirmed": 350,
      "completed": 180,
      "cancelled": 45,
      "pending": 20,
      "no_show": 25
    },
    "top_specialties": [
      { "name": "قلب و عروق", "count": 120 }
    ]
  }
}

GET /api/v1/admin/dashboard/recent

Get recent activity (last 10 of each type).

Permission: ROLE_ADMIN

Response 200

{
  "success": true,
  "data": {
    "appointments": [
      {
        "uuid": "...",
        "doctor_title": "دکتر علی احمدی",
        "patient_name": "محمد رضایی",
        "slot_start": 1718438400,
        "status": "confirmed"
      }
    ],
    "payments": [
      {
        "uuid": "...",
        "amount_rials": 500000,
        "gateway": "mellat",
        "status": "paid",
        "created_at": 1717000000
      }
    ],
    "users": [
      {
        "uuid": "...",
        "real_name": "محمد رضایی",
        "mobile_number": "09...",
        "roles": ["ROLE_USER"],
        "created_at": 1717000000
      }
    ]
  }
}

User Management

GET /api/v1/admin/users

List all users with pagination and filters.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 20
search string Search by name or mobile
role string فیلتر نقش: admin | doctor | secretary | clinic | representation (کاربران دارای ROLE_REPRESENTATION) | patient
status string "active" or "inactive"
sort string "created_at" (default desc)

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "real_name": "علی احمدی",
      "mobile_number": "09123456789",
      "roles": ["ROLE_USER"],
      "status": "active",
      "created_at": 1717000000
    }
  ],
  "meta": { "totalRecords": 1200, "totalPages": 60, "currentPage": 1 }
}

GET /api/v1/admin/users/{uuid}

Get detailed user info.

Permission: ROLE_ADMIN

Response 200

{
  "success": true,
  "data": {
    "uuid": "...",
    "real_name": "علی احمدی",
    "mobile_number": "09123456789",
    "roles": ["ROLE_USER"],
    "status": "active",
    "wallet_balance_rials": 500000,
    "appointments_count": 5,
    "created_at": 1717000000
  }
}

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 User not found

GET /api/v1/admin/users/stats

Get user statistics.

Permission: ROLE_ADMIN

Response 200

{
  "success": true,
  "data": {
    "total": 1200,
    "active": 1150,
    "inactive": 50,
    "admins": 3,
    "doctors": 92,
    "patients": 1100
  }
}

PUT /api/v1/admin/users/{uuid}

Update user info (name, email, password).

Permission: ROLE_ADMIN

Request Body (application/json)

{
  "real_name": "علی احمدی جدید",
  "password": "newPassword123"
}

Response 200

Updated user object.


PUT /api/v1/admin/users/{uuid}/role

Change a user's role.

Permission: ROLE_ADMIN

Request Body (application/json)

{
  "role": "ROLE_DOCTOR"
}
Field Type Required Allowed Values
role string ROLE_USER, ROLE_DOCTOR, ROLE_CLINIC, ROLE_SECRETARY, ROLE_ADMIN

Response 200

{ "success": true, "data": { "message": "نقش کاربر تغییر کرد", "roles": ["ROLE_DOCTOR"] } }

POST /api/v1/admin/users/{uuid}/status

Toggle user active/inactive status.

Permission: ROLE_ADMIN

Response 200

{ "success": true, "data": { "status": "inactive" } }

DELETE /api/v1/admin/users/{uuid}

Delete a user.

Permission: ROLE_ADMIN

Response 200

{ "success": true, "data": { "message": "کاربر حذف شد" } }

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 User not found

Doctor Management

نماینده (ROLE_REPRESENTATION): افزودن پزشک و کلینیک برای نماینده از طریق endpointهای جدا انجام می‌شود — POST /api/v1/representation/doctor و POST /api/v1/representation/clinic (به docs/api/representation.md مراجعه کنید). در نسخه‌ی نماینده، representation_id پزشک خودکار روی نماینده‌ی کاربر جاری ست می‌شود. endpointهای /api/v1/admin/* همچنان فقط ROLE_ADMIN هستند.

POST /api/v1/admin/doctors

ساخت پزشک جدید (در صورت نبودِ کاربر با این موبایل، یک User هم ساخته می‌شود).

Permission: ROLE_ADMIN

Request Body (application/json)

Field Type Required Description
mobile string موبایل ورود؛ باید فرمت معتبر موبایل ایران داشته باشد (^09\d{9}$) — ارقام فارسی/عربی به انگلیسی نرمال می‌شوند
name string نام پزشک
gender / degree / medical_system_code / info string اطلاعات حرفه‌ای
activity_time integer Unix timestamp (ثانیه) تاریخ شروع فعالیت؛ مبنای محاسبهٔ سال تجربه
specialties integer[] آرایهٔ IDهای تخصص. هر شناسهٔ فرزند سمت سرور با تمام والدهایش تا ریشهٔ درخت گسترش می‌یابد؛ شناسه‌های ناموجود نادیده گرفته می‌شوند.

Errors

Code HTTP Description
VALIDATION 422 mobile یا name خالی
VALIDATION 422 mobile فرمت معتبر موبایل ایران ندارد (field: mobile)

POST /api/v1/admin/clinic

ساخت کلینیک جدید (در صورت نبودِ کاربرِ صاحب با این موبایل، یک User هم ساخته می‌شود).

Permission: ROLE_ADMIN

Request Body (application/json)

Field Type Required Description
owner_mobile string موبایل صاحب کلینیک؛ باید فرمت معتبر موبایل ایران داشته باشد (^09\d{9}$)
name string نام کلینیک

Errors

Code HTTP Description
VALIDATION 422 owner_mobile خالی
VALIDATION 422 owner_mobile فرمت معتبر موبایل ایران ندارد (field: owner_mobile)
VALIDATION 422 name کلینیک خالی

GET /api/v1/admin/doctors

List all doctors with pagination.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 20
search string Search in title
status string "active" or "inactive"
gender string "male" or "female"
specialty_id integer Filter by specialty
owner_status string claimed | unclaimed | pending_transfer — پروفایل‌های ایمپورت IRIMC (خروجی هم owner_status و source دارد)
unassigned string 1 → فقط پزشکانِ بدون نماینده (representation_id IS NULL) — برای انتخاب و اتصال به نماینده
city_id integer فیلتر بر اساس شهرِ آدرسِ خودِ پزشک (doctor_addresses.city_id)
state_id integer فیلتر بر اساس استانِ آدرسِ خودِ پزشک (doctor_addresses.province_id)
sort string Sort field

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "title": "علی احمدی",
      "degree": "متخصص",
      "gender": "male",
      "doctor_rate": 4.5,
      "active_doctor_appointment": true,
      "representation_id": 12,
      "representation_uuid": "9c1...",
      "representation_name": "علی محمدی"
    }
  ],
  "meta": { "totalRecords": 92, "totalPages": 5, "currentPage": 1 }
}

representation_id/representation_uuid/representation_name نماینده‌ی مالکِ پزشک‌اند؛ برای پزشکانِ بدون نماینده (مثل ایمپورت‌های IRIMC) هر سه null.


GET /api/v1/admin/doctors/stats

Get doctor statistics.

Permission: ROLE_ADMIN

Response 200

{
  "success": true,
  "data": {
    "total": 92,
    "active": 85,
    "inactive": 7,
    "male": 60,
    "female": 32,
    "top_specialty": "قلب و عروق"
  }
}

POST /api/v1/admin/doctors/{uuid}/status

Toggle doctor active status.

Permission: ROLE_ADMIN

Response 200

{ "success": true, "data": { "active": false } }

PUT /api/v1/admin/doctors/{uuid}/representation

ست کردن یا حذف نماینده‌ی یک پزشک (از صفحه‌ی پروفایل پزشک در پنل ادمین).

Permission: ROLE_ADMIN

Request Body

{ "representation_id": 12 }

representation_id: null (یا حذف‌شده/خالی) → نماینده‌ی پزشک حذف می‌شود (representation_id = NULL). بر خلاف POST /representations/{uuid}/doctors، این endpoint اجازه‌ی تغییر نماینده‌ی پزشکی که از قبل نماینده دارد را هم می‌دهد.

Response 200

{
  "success": true,
  "data": {
    "message": "نماینده ثبت شد",
    "representation": { "id": 12, "uuid": "9c1...", "full_name": "علی محمدی" }
  }
}

برای حذف، representation برابر null برمی‌گردد.

Errors

Code HTTP Description
DOCTOR_NOT_FOUND 404 پزشک یافت نشد
ERR_NOT_FOUND_001 404 نماینده یافت نشد

Clinic Management

GET /api/v1/admin/clinics

List all clinics with pagination.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 20
search string Search by clinic name
status string "active" or "inactive"

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "name": "کلینیک الوند",
      "city": "تهران",
      "telephone": "02112345678",
      "is_active": true,
      "created_at": 1717000000
    }
  ],
  "meta": { "totalRecords": 34, "totalPages": 2, "currentPage": 1 }
}

PATCH /api/v1/admin/clinic/{uuid}/status

Toggle clinic active/inactive.

Permission: ROLE_ADMIN

Response 200

{ "success": true, "data": { "is_active": false } }

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 Clinic not found

DELETE /api/v1/admin/clinic/{uuid}

Delete a clinic.

Permission: ROLE_ADMIN

Side effect: the clinic's insurance configuration (tenant_insurances, entity_insurance_pricing, and their tenant_service_coverages) is purged in the same request — polymorphic entity_id, cleaned up at the application level.

Response 200

{ "success": true, "data": { "message": "کلینیک حذف شد" } }

Appointment Management

GET /api/v1/admin/appointments/today-stats

Get appointment statistics for a specific date (defaults to today).

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
date string (YYYY-MM-DD) Default: today

Response 200

{
  "success": true,
  "data": {
    "total": 47,
    "completed": 20,
    "waiting": 18,
    "cancelled": 9
  }
}

GET /api/v1/admin/appointments

List appointments filtered by date and/or doctor. Sorted by slot_start ASC.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 15, max: 500
search string Search by patient name/mobile or doctor name
status string Filter by status
date string (YYYY-MM-DD) Filter by slot date
doctor_uuid string Filter by doctor UUID

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "appt-uuid",
      "patient_name": "محمد رضایی",
      "patient_mobile": "09123456789",
      "doctor_uuid": "doctor-uuid",
      "doctor_name": "علی احمدی",
      "slot_start": 1718438400,
      "slot_end": 1718439600,
      "appointment_date": "2025-06-15",
      "appointment_time": "09:00",
      "end_time": "09:20",
      "status": "confirmed",
      "version": 1,
      "created_at": "2025-06-14T10:30:00+03:30"
    }
  ],
  "meta": { "totalRecords": 47, "totalPages": 1, "currentPage": 1 }
}

Status values: pending | confirmed | completed | cancelled_by_doctor | cancelled_by_user | no_show | expired


POST /api/v1/admin/appointment

Create a new appointment for a patient. If no user exists with the given mobile, a new user account is created automatically.

Permission: ROLE_ADMIN

Request Body

{
  "doctor_uuid": "doctor-uuid",
  "slot_start": 1718438400,
  "slot_end": 1718439600,
  "patient_mobile": "09123456789",
  "patient_name": "علی محمدی",
  "patient_national_code": "0012345678",
  "service_item_uuids": ["service-uuid-1", "service-uuid-2"],
  "duration_from_services": false,
  "service_durations": { "service-uuid-1": 75 },
  "visit_price_rials": 3000000,
  "note": "optional note"
}

patient_mobile، patient_name و patient_national_code هر سه اجباری هستند. کد ملی باید ۱۰ رقم معتبر باشد و روی پروفایل بیمار ذخیره می‌شود (profiles.national_code، یکتا). بیمار اول با کد ملیِ پروفایل و سپس با موبایل resolve می‌شود، تا پرونده برای هر کد ملی یکتا بماند (یک شخص می‌تواند چند موبایل داشته باشد). اگر بیماری یافت نشود، کاربر جدید (ROLE_USER) به‌همراه پروفایلِ حاملِ همان کد ملی ساخته می‌شود.

service_item_uuids[] (اختیاری): یک یا چند سرویس که به نوبت پیوست می‌شوند؛ اولین سرویس = سرویسِ اصلی و همه در service_items پاسخ برمی‌گردند. UUID ناموجود ⇒ 422. با duration_from_services: true مدت نوبت از مجموع duration_minutes سرویس‌ها محاسبه و slot_end بازنویسی می‌شود (سرویسِ غیرbookable/بدون مدت ⇒ 422)؛ بدون آن ساعت پایانِ دستی حفظ می‌ماند. service_durations ({ "<uuid>": <minutes> }، فقط با duration_from_services=true): override مدتِ هر سرویس برای همان نوبت؛ پیش‌فرضِ سرویس در تنظیمات تغییر نمی‌کند.

visit_price_rials (اختیاری/شرطی): هزینه ویزیت (ریال) که روی نوبت ذخیره می‌شود. اگر فلگ require_visit_price در insurance-pricing برای پزشک (یا کلینیکِ واحد او در نبود ردیف پزشک) فعال باشد، مقدار > 0 الزامی است.

Response 201

{
  "success": true,
  "data": {
    "uuid": "appt-uuid",
    "slot_start": 1718438400,
    "slot_end": 1718439600,
    "status": "pending"
  }
}

Error Responses

Code HTTP Description
VALIDATION 422 Missing required fields (doctor_uuid, slot_start, slot_end, patient_mobile, patient_name), or missing/invalid patient_national_code (field: patient_national_code), or required visit_price_rials <= 0 when require_visit_price is on (field: visit_price_rials)
ERR_PROFILE_MOBILE_TAKEN 422 این شماره موبایل با کد ملی دیگری ثبت شده است (field: patient_mobile)
DOCTOR_NOT_FOUND 404 Doctor UUID not found
SLOT_TAKEN 409 Slot already booked

Payment Management

GET /api/v1/admin/payments

List all payments.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 20
status string "pending", "paid", "failed", "cancelled"

Query Parameters (تکمیل)

Param Type Required Description
search string جستجو در موبایل کاربر، reference_id یا order_id

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "amount": 500000,
      "status": "success",
      "gateway": "mellat",
      "ref_id": "1234567",
      "patient_mobile": "0912...",
      "paid_at": "2026-07-02T09:00:00+03:30",
      "created_at": "2026-07-02T08:55:00+03:30"
    }
  ],
  "meta": { "totalRecords": 7800, "totalPages": 390, "currentPage": 1 }
}

amount بر حسب ریال، ref_id همان reference_id درگاه، paid_at فقط برای پرداخت success (بر اساس updated_at) و در غیر این‌صورت null. تاریخ‌ها ISO-8601.


GET /api/v1/admin/payments/{uuid}

جزئیات یک پرداخت. پاسخ تخت است (data مستقیم آبجکت پرداخت، نه nested).

Permission: ROLE_ADMIN

Response 200

{
  "success": true,
  "data": {
    "uuid": "...",
    "order_id": "ORD-XXXX",
    "amount": 500000,
    "status": "success",
    "gateway": "mellat",
    "type": "appointment",
    "ref_id": "1234567",
    "card_pan": "502229******2928",
    "patient_mobile": "0912...",
    "patient_name": "علی احمدی",
    "appointment_uuid": "...",
    "paid_at": "2026-07-02T09:00:00+03:30",
    "created_at": "2026-07-02T08:55:00+03:30"
  }
}

card_pan شمارهٔ کارت ماسک‌شدهٔ پرداخت‌کننده (۶ رقم اول + ۴ رقم آخر) است که درگاه در callback برمی‌گرداند (ملت: CardHolderPan) و در metadata.card_pan پرداخت ذخیره می‌شود؛ اگر درگاه آن را نفرستد null. refunds[] تاریخچهٔ استردادها (amount ریال، ref شماره پیگیری، at unix).

POST /api/v1/admin/payments/{uuid}/refund

استرداد وجه یک پرداخت موفق (کل یا جزئی). فقط ROLE_ADMIN. فقط درگاه ملت پشتیبانی می‌شود (سپ خطا می‌دهد).

Request body:

فیلد نوع توضیح
amount integer? مبلغ استرداد به ریال. اگر ندهی = کل باقی‌ماندهٔ قابل استرداد.

استرداد جزئی چندباره مجاز است تا سقف مبلغ خرید. استرداد کامل (رسیدن جمع به مبلغ کل) وضعیت را refunded می‌کند.

Response 200:

{ "success": true, "data": { "status": "refunded", "refund_ref": "183800538958" } }

کد 0 درگاه ملت فقط «پذیرش اولیهٔ درخواست استرداد» است؛ عودت نهایی به کارت ممکن است چند روز طول بکشد.

POST /api/v1/admin/payments/{uuid}/reverse

برگشت وجه یک پرداخت موفقِ settle‌نشده (بدون body). فقط ROLE_ADMIN. در موفقیت وضعیت refunded.

Response 200: { "success": true, "data": { "status": "refunded" } }

Errors (payment refund/reverse/detail)

Code HTTP Description
ERR_NOT_FOUND_001 404 پرداخت یافت نشد
ERR_PAYMENT_002 422 مبلغ نامعتبر / پرداخت غیرقابل استرداد / درگاه پشتیبانی نمی‌کند

Settlement Management

GET /api/v1/admin/settlements

List all settlement requests.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 20
status string "pending", "approved", "rejected"

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "user": { "uuid": "...", "real_name": "علی احمدی" },
      "amount_rials": 1000000,
      "status": "pending",
      "bank_account": { "bank_name": "بانک ملت", "owner_name": "..." },
      "created_at": 1717000000
    }
  ],
  "meta": { "totalRecords": 45, "totalPages": 3, "currentPage": 1 }
}

To approve or reject, use the Settlement API: POST /api/v1/settlement/{uuid}/approve or /reject


Representation Management

GET /api/v1/admin/representations

List all representations.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 15
search string Search by name or mobile (representation's or linked user's)
city_id integer Filter by city (عضویت در شهرهای چندگانه‌ی نماینده — representation_cities)

Response 200

Paginated representation list. Each item:

{
  "id": 3,
  "uuid": "...",
  "full_name": "حامد حسینی",
  "mobile_number": "09120671756",
  "domain": "x-nobat.ir",
  "is_global": true,
  "city_id": 132,
  "city_ids": [132, 108],
  "cities": [{ "id": 132, "name": "یزد" }, { "id": 108, "name": "تهران" }],
  "city": "یزد، تهران",
  "commission_percent": 10.0,
  "wallet_balance": 0,
  "doctor_count": 14,
  "appointment_count": 231,
  "is_active": true,
  "created_at": "2026-06-18T..."
}

city_id = اولین شهر (BC)؛ city = نام شهرها با «،». is_global=true یعنی نماینده سراسری (badge در پنل).

doctor_count = تعداد پزشکانِ representation_id = r.id؛ appointment_count = تعداد نوبت‌های آن پزشکان. هر دو با کوئری گروهی محاسبه می‌شوند (بدون N+1).

mobile_number falls back to the linked user's mobile when the representation's own mobile_number column is empty.

Deactivation, not deletion: the admin panel deactivates a representation via PATCH /api/v1/representation/{uuid} with { "active": false } rather than calling DELETE.


GET /api/v1/admin/representations/{uuid}/doctors

پزشکان زیرمجموعه‌ی یک نماینده (paginated).

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 15 (max 100)

Response 200

Paginated. Each item:

{
  "uuid": "...",
  "id": 45,
  "name": "علی احمدی",
  "gender": "man",
  "medical_code": "12345",
  "is_active": true,
  "owner_status": "claimed",
  "past_count": 12,
  "upcoming_count": 5,
  "created_at": "2026-06-18T..."
}

past_count = نوبت‌های slot_start < now؛ upcoming_count = slot_start >= now.

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 نماینده یافت نشد

POST /api/v1/admin/representations/{uuid}/doctors

اتصال یک پزشکِ موجودِ بدون نماینده به این نماینده (representation_id ست می‌شود).

Permission: ROLE_ADMIN

Request Body

{ "doctor_uuid": "550e8400-..." }

Response 200

{ "success": true, "data": { "message": "پزشک به نماینده متصل شد", "doctor_uuid": "550e8400-...", "representation_id": 12 } }

Errors

Code HTTP Description
ERR_VALIDATION_001 422 doctor_uuid ارسال نشده
ERR_NOT_FOUND_001 404 نماینده یا پزشک یافت نشد
ERR_CONFLICT_001 409 پزشک از قبل به یک نماینده متصل است

GET /api/v1/admin/representations/{uuid}/appointments

نوبت‌های پزشکانِ زیرمجموعه‌ی یک نماینده (paginated).

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
scope string upcoming (پیش‌فرض، slot_start >= now، صعودی) یا past (slot_start < now، نزولی)
page integer Default: 1
limit integer Default: 15 (max 100)

Response 200

Paginated. Each item:

{
  "uuid": "...",
  "slot_start": 1750000000,
  "slot_end": 1750001800,
  "status": "confirmed",
  "patient_name": "علی رضایی",
  "doctor_uuid": "...",
  "doctor_name": "علی احمدی"
}

slot_start/slot_end Unix timestamp (ثانیه).

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 نماینده یافت نشد

Secretary Management

GET /api/v1/admin/secretaries

List all secretaries.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 20
search string Search by mobile

Response 200

Paginated secretary list with linked doctor info. هر ردیف علاوه بر مجوزها، online_share_enabled و online_share_percent (سهم منشی از نوبت‌های آنلاین) را هم دارد.


GET /api/v1/admin/secretary/{uuid}

جزئیات یک رابطهٔ منشی–پزشک/کلینیک (uuid = DoctorSecretary.uuid، همان uuid لیست بالا) به‌همراه تنظیمات سهم و خلاصهٔ درآمد.

Permission: ROLE_ADMIN

Response 200

{
  "success": true,
  "data": {
    "data": {
      "uuid": "rel-uuid-…",
      "secretary_uuid": "user-uuid-…",
      "user_name": "زهرا رضایی",
      "mobile_number": "0912…",
      "doctor_name": "دکتر احمدی",
      "doctor_uuid": "doc-uuid-…",
      "owner_type": "doctor",
      "clinic_uuid": null,
      "clinic_name": null,
      "is_active": true,
      "online_share_enabled": true,
      "online_share_percent": 5,
      "permissions": { "…": {} },
      "created_at": 1700000000,
      "earnings": {
        "total_rials": 4500000,
        "this_month_rials": 1500000,
        "appointments_count": 9
      }
    }
  }
}

earnings روی کاربرِ منشی جمع می‌شود (نه فقط این رابطه): مجموع همهٔ سهم‌های ثبت‌شده در secretary_earnings. this_month_rials = ۳۰ روز گذشته.

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_AUTH_006 403 Not admin
ERR_NOT_FOUND_001 404 منشی یافت نشد

PUT /api/v1/admin/secretary/{uuid}/online-share

فعال/غیرفعال‌کردن محاسبهٔ درآمد منشی از نوبت‌های آنلاین و تعیین درصد سهم. تنظیم per-relation است: یک منشی می‌تواند برای یک پزشک سهم داشته باشد و برای دیگری نه.

Permission: ROLE_ADMIN

Request Body (application/json)

{ "enabled": true, "percent": 5 }
Field Type Required Description
enabled boolean محاسبهٔ سهم برای این رابطه فعال باشد؟
percent number درصد سهم از مبلغ خالص نوبت (۰ تا ۱۰۰)

Response 200

همان شکل رابطه (DoctorSecretary::toArray()) پس از ذخیره.

Errors

Code HTTP Description
ERR_AUTH_006 403 Not admin
ERR_NOT_FOUND_001 404 منشی یافت نشد
ERR_VALIDATION_001 422 percent خارج از ۰–۱۰۰ (field: percent)
ERR_VALIDATION_001 422 enabled=true با percent=0 (field: percent)

مبنای محاسبه: سهم منشی مثل پورسانت نماینده از «خالصِ پس از مالیات» گرفته می‌شود — ابتدا هزینهٔ پنل پیامک، بعد مالیات، بعد سهم‌ها. تنها نوبت‌هایی که آنلاین پرداخت می‌شوند سهم می‌سازند (نوبت ثبت‌شده در پنل از مسیر تقسیم مالی عبور نمی‌کند). جزئیات: settlement.md و secretary.md.


Rating & Comment Management

GET /api/v1/admin/rates

List all ratings.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 20
search string Search by doctor/patient

Ratings are multi-dimensional (five 0100 dimensions). Each row's overall is the mean of the five dimensions (0100) and score is that mean on a 05 scale (overall / 20).


GET /api/v1/admin/comments

List all comments (all statuses).

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 20
search string Search in body
status string "pending", "approved", "rejected"

To approve/reject comments, use the Rating API: POST /api/v1/admin/comment/{uuid}/approve or /reject


SMS Management (Admin)

GET /api/v1/admin/sms/logs

List SMS send logs.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 15, max 100
tag string فیلتر بر اساس تگ: global | otp | payment | clinic_invitation | pre_registration | notification_mobile | user_template

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "recipient": "09123456789",
      "message": "کد تأیید: 123456",
      "status": "sent",
      "provider": "kavenegar",
      "tag": "otp",
      "template": "clinicpro-otp",
      "sent_at": "2026-06-19T...",
      "created_at": "2026-06-19T..."
    }
  ],
  "meta": { "totalRecords": 5000, "totalPages": 334, "currentPage": 1 }
}

tag نوع پیامک را مشخص می‌کند؛ پیش‌فرض پیامک‌های سیستمیِ بی‌برچسب global است. template نام الگوی VerifyLookup کاوه‌نگار است که پیامک با آن ارسال شده (مثلاً clinicpro-otpnull اگر ارسال نشده باشد (مثلاً رد به‌دلیل نبود الگو در سیاست lookup-only).


GET /api/v1/admin/sms/templates

List all SMS templates.

Permission: ROLE_ADMIN

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "name": "تأیید نوبت",
      "status": "approved",
      "provider_code": "verify_appointment",
      "created_at": 1717000000
    }
  ]
}

To create/approve/reject templates, see sms.md


Clinic Invitation Management

See clinic-invitation.md for full endpoint details.

Endpoint Description
POST /api/v1/admin/clinic/{uuid}/invite-doctor Send invitation
GET /api/v1/admin/clinic/{uuid}/invitations List invitations
POST /api/v1/admin/clinic/invitation/{invUuid}/resend Resend SMS
PATCH /api/v1/admin/clinic/invitation/{invUuid}/status Change status
DELETE /api/v1/admin/clinic/invitation/{invUuid} Delete

Settings

GET /api/v1/admin/settings

Returns all site configuration values.

Response 200

{
  "success": true,
  "data": {
    "commission_enabled": "0",
    "commission_percent": "0",
    "site_name": "ClinicPro",
    "support_phone": "",
    "max_cancel_hours_before": "24",
    "appointment_reminder_hours": "2",
    "log_retention_days": "90",
    "payment_test_mode": "0",
    "mellat_enabled": "1",
    "mellat_terminal_id": "",
    "mellat_username": "",
    "mellat_password": "",
    "sep_enabled": "1",
    "sep_terminal_id": "",
    "sms_provider": "kavenegar",
    "kavenegar_api_key": "",
    "kavenegar_sender": "",
    "sms_price_rials": "500"
  }
}

All values are strings. Missing keys return their default values.

PATCH /api/v1/admin/settings

Update one or more settings. Unknown keys are silently ignored.

Request body (partial update — send only keys to change):

{
  "commission_enabled": "1",
  "commission_percent": "5",
  "site_name": "کلینیک‌پرو",
  "payment_test_mode": "1",
  "mellat_terminal_id": "12345678",
  "mellat_username": "user",
  "mellat_password": "pass",
  "sep_terminal_id": "87654321",
  "sms_provider": "kavenegar",
  "kavenegar_api_key": "your-api-key",
  "kavenegar_sender": "10008664",
  "sms_price_rials": "500"
}

Response 200 — same shape as GET, returns all settings after save.

Commission rules:

  • commission_enabled"1" = active, "0" = inactive
  • commission_percent — integer string, 0100
  • Commission applies only to regular users (booked_by = user); secretaries are exempt

Payment gateway rules:

  • payment_test_mode"1" = all payments use MockGateway (no real bank calls), "0" = real gateways
  • mellat_enabled / sep_enabled"1" = gateway selectable, "0" = gateway disabled (hidden from the payment selection list and rejected at initiate). Unset = enabled (default).
  • payment_allowed_frontend_hosts — comma-separated hosts allowed as a payment frontend_address (origin site to return to). Falls back to the ALLOWED_FRONTEND_HOSTS env var when empty. Add a consumer site's host here to permit its payments.
  • Gateway credentials (mellat/sep) read from DB first, fallback to env vars if DB value is empty
  • MockGateway callback: same URL pattern + &mock=1&ResCode=0&RefId=MOCK-{orderId} (add &cancel=1 to simulate a user cancellation → canceled)

SMS provider rules:

  • sms_provider"kavenegar" or "rangineh"
  • Kavenegar API key and sender read from DB first, fallback to env vars KAVENEGAR_API_KEY, KAVENEGAR_SENDER

Maintenance Mode

A single switch that takes the whole platform offline — the public site, the admin SPA's data calls, every /api/v1/* endpoint, and therefore nobat724_front and clinic-pro-tauri too. Enforced centrally by App\Shared\EventSubscriber\MaintenanceSubscriber; no controller checks it itself.

Settings keys

Managed through the same GET/PATCH /api/v1/admin/settings endpoints (whitelisted in SiteConfigController::ALLOWED_KEYS). Admin panel: /admin/settings → بخش «حالت تعمیرات».

Key Default Description
maintenance_enabled "0" "1"/"true"/"on"/"yes" = maintenance active
maintenance_title در حال به‌روزرسانی سیستم Heading of the HTML maintenance page
maintenance_message سامانه موقتاً ... Shown both on the HTML page and as the API error message
maintenance_retry_after "600" Seconds; sent as the Retry-After response header
maintenance_allowed_ips "" Comma-separated IPs that bypass maintenance without logging in

Changing any maintenance_* key invalidates the 30-second MaintenanceService cache immediately, so a toggle takes effect on the next request.

Behaviour while enabled

API requests (path starts with /api/, or Accept: application/json, or X-Requested-With: XMLHttpRequest):

HTTP/1.1 503 Service Unavailable
Retry-After: 600

{
  "success": false,
  "data": null,
  "errors": [
    { "code": "MAINTENANCE_MODE", "message": "<maintenance_message>" }
  ]
}

The envelope is identical to BaseController::error(), so existing clients parse it unchanged. Clients should detect maintenance by both status === 503 and errors[0].code === "MAINTENANCE_MODE" — a bare 503 may come from a reverse proxy.

Browser requeststemplates/maintenance.html.twig rendered with HTTP 503, same Retry-After header, noindex, nofollow.

Both the kernel.request (priority 6) and kernel.exception (priority 20) paths are covered, so routing 404/405 and firewall 401 responses also return maintenance rather than leaking their normal errors.

Who gets through

  1. Whitelisted paths — never blocked, in this order of importance: /oauth/*, /api/v1/user/{login,send-code,verify-code,otp-login}, /session/token (admins must still be able to sign in), /api/v1/admin/settings (the only way to turn maintenance back off), /health, /admin* (the SPA shell HTML — its data calls are still guarded), /build/*, /favicon.ico, /_wdt, /_profiler.
  2. maintenance_allowed_ips — exact client-IP match.
  3. ROLE_ADMIN — resolved from the firewall token. Several admin-panel endpoints (/api/v1/doctors, /api/v1/categorys/*, …) live in the public_endpoints firewall with security: false, where no token is ever resolved; for those the subscriber falls back to decoding the Authorization: Bearer JWT and checking its roles claim. An invalid or forged token does not bypass.

Every other role — ROLE_DOCTOR, ROLE_CLINIC, ROLE_SECRETARY, ROLE_REPRESENTATION — is blocked.

Console escape hatch

If the admin panel is unreachable:

ddev exec php bin/console app:maintenance status
ddev exec php bin/console app:maintenance on
ddev exec php bin/console app:maintenance off

Failure behaviour

MaintenanceService is fail-open: if Redis is unavailable it reads straight from the database, and if the database also fails it reports maintenance as disabled. This layer must never become the cause of an outage.


Pre-Registration Management

GET /api/v1/admin/pre-registrations

List pre-registration requests. Permission: ROLE_ADMIN

Query params:

Param Default Notes
page 1
limit 20 max 50
status pending pending | approved | rejected | all

Response 200 (paginated):

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "type": "independent_doctor",
      "name": "احمدی",
      "mobile": "09121234567",
      "info": "متخصص داخلی",
      "status": "pending",
      "admin_note": null,
      "created_at": 1718000000
    }
  ],
  "meta": { "totalRecords": 5, "totalPages": 1, "currentPage": 1 }
}

POST /api/v1/admin/pre-registrations/{uuid}/approve

Approve a pending request. Creates User + Doctor/Clinic entity based on type, resets password, sends SMS. Permission: ROLE_ADMIN

Response 200:

{ "success": true, "data": { "message": "تأیید شد و اطلاعات ورود ارسال گردید" } }

Error Codes:

Code HTTP Meaning
NOT_FOUND 404 UUID not found
ALREADY_PROCESSED 409 Status is not pending

POST /api/v1/admin/pre-registrations/{uuid}/reject

Reject a pending request. Permission: ROLE_ADMIN

Request body (optional):

{ "note": "مدارک ناقص است" }

Response 200:

{ "success": true, "data": { "message": "درخواست رد شد" } }

موتور مالی نمایندگی

تنظیمات مالی از طریق GET/PATCH /api/v1/admin/settings کنترل می‌شوند (کلیدها در whitelist SiteConfigController::ALLOWED_KEYS):

کلید پیش‌فرض شرح
appointment_commission_enabled 0 فعال‌سازی پورسانت نوبت (درصد از Representation.commission_percent هر نماینده)
upgrade_commission_enabled 0 فعال‌سازی پورسانت ارتقاء اشتراک
upgrade_commission_percent 20 درصد پورسانت ارتقاء (سراسری)
tax_enabled 0 فعال‌سازی مالیات بر ارزش افزوده
tax_percent 10 درصد مالیات
sms_panel_fee_rials 1500000 هزینه ثابت پنل پیامک به ریال (از نوبت و اشتراک کسر می‌شود)
sms_price_rials 500 هزینه هر پیامک ارسالی به ریال؛ مبنای محاسبهٔ تعداد پیامک از موجودی کیف‌پول (GET /api/v1/sms/wallet/balance). قابل ویرایش در /admin/settings → بخش پیامک
appointment_fee_rials 150000 مبلغ هر نوبت به ریال؛ مبلغی که بیمار هنگام رزرو آنلاین پرداخت می‌کند. backend از همین کلید می‌خواند و در GET /api/v1/payment/config expose می‌شود
log_retention_days 90 مدت نگهداری لاگ‌ها (روز)؛ کاماند روزانه app:prune-logs لاگ‌های قدیمی‌تر را حذف می‌کند. 0 = نگهداری نامحدود
altcha_enabled (از env) فعال/غیرفعال کپچای ALTCHA (1/0). اگر ست شود بر ALTCHA_ENABLED env مقدم است؛ اگر ست نشده باشد GET مقدار مؤثرِ env را برمی‌گرداند. رجوع به captcha.md

ترتیب محاسبه (در CommissionService): ۱) کسر sms_panel_fee_rials ۲) مالیاتِ استخراجی afterSms × tax/(100+tax) ۳) پورسانت = netAfterTax × percent/100. سهم نماینده به کیف‌پولش (WalletTransaction credit) واریز و یک ردیف FinancialBreakdown ثبت می‌شود (idempotent بر اساس payment_id).

هر تغییر tax_percent/tax_enabled در یک ردیف TaxRateHistory ثبت و از GET /api/v1/admin/settings/tax-history قابل مشاهده است.

GET /api/v1/admin/financial-breakdowns

لیست تفکیک مالی تراکنش‌ها (paginated). Permission: ROLE_ADMIN

Query Parameters:

Param Type Description
page integer پیش‌فرض 1
limit integer پیش‌فرض 15، حداکثر 100
representation_id integer فیلتر نماینده
source string appointment یا subscription
from integer Unix timestamp شروع بازه
to integer Unix timestamp پایان بازه

Response 200 (paginated):

{
  "success": true,
  "data": [
    {
      "uuid": "…", "order_id": "ORD-…", "source": "appointment",
      "gross_rials": 2000000, "sms_fee_rials": 1500000,
      "tax_percent": 10, "tax_rials": 45455, "net_after_tax_rials": 454545,
      "commission_percent": 20, "representation_share_rials": 90909,
      "system_share_rials": 363636,
      "representation_id": 3, "representation_name": "نماینده یزد",
      "doctor_id": 12, "clinic_id": null, "created_at": "2026-06-24T…"
    }
  ],
  "meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}

GET /api/v1/admin/financial-summary

جمع کل مبالغ. Permission: ROLE_ADMIN

Response 200:

{
  "success": true,
  "data": {
    "total_gross": 2000000,
    "total_representation_income": 90909,
    "total_tax_collected": 45455,
    "total_sms_fee": 1500000,
    "total_system_share": 363636
  }
}

GET /api/v1/admin/settings/tax-history

تاریخچه‌ی تغییرات مالیات بر ارزش افزوده (۵۰ ردیف آخر، نزولی). هر بار که tax_percent یا tax_enabled از طریق PATCH /api/v1/admin/settings تغییر کند، یک ردیف با کاربرِ تغییردهنده ثبت می‌شود. Permission: ROLE_ADMIN

Response 200:

{
  "success": true,
  "data": [
    { "tax_percent": 10, "enabled": true, "changed_by_name": "مدیر سیستم", "changed_at": 1782800000 }
  ]
}

GET /api/v1/admin/settlement/{uuid}

جزئیات یک درخواست تسویه (برای صفحه‌ی /admin/settlements/{uuid}). Permission: ROLE_ADMIN

Response 200:

{
  "success": true,
  "data": {
    "uuid": "...",
    "representation_name": "نماینده یزد",
    "representation_mobile": "09390036732",
    "amount": 500000,
    "status": "pending",
    "bank_card": "6037...", "bank_name": "ملت", "bank_iban": "IR...", "bank_owner": "...",
    "reject_reason": null,
    "requested_at": "2026-06-24T...", "processed_at": null
  }
}

تأیید/رد از طریق POST /api/v1/settlement/{uuid}/approve|reject (در docs/api/settlement.md).

Errors: NOT_FOUND (404) — درخواست یافت نشد.


Application Logs

Persisted application logs (warning level and above). Written by the DbLogger decorator over the logger service into the app_log table — every LoggerInterface::warning()/error()/critical()/... call across the backend lands here, while info/debug go to stderr only.

GET /api/v1/admin/logs

List persisted logs with pagination and filters.

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 25 (max 100)
level string Exact PSR level: warning, error, critical, alert, emergency
search string Substring match on the message
from integer Unix timestamp lower bound (created_at >=)
to integer Unix timestamp upper bound (created_at <=)

Ordered by newest first (id DESC).

Response 200

{
  "success": true,
  "data": [
    {
      "id": 4213,
      "level": "error",
      "message": "Unhandled exception: RuntimeException: boom @ /var/www/html/src/Foo.php:42 [path=/oauth/userinfo]",
      "context": "{\"exception\":\"RuntimeException: boom @ /var/www/html/src/Foo.php:42\"}",
      "channel": "app",
      "path": "/oauth/userinfo",
      "created_at": 1717000000
    }
  ],
  "meta": { "totalRecords": 137, "totalPages": 6, "currentPage": 1 }
}

Notes:

  • context is a JSON string (or null); a Throwable in the context is stored as a compact Class: message @ file:line string, never the raw object.
  • created_at is a Unix timestamp (integer).

GET /api/v1/admin/logs/export

Export all matching logs as a CSV file (no pagination). Respects the same level, search, from, to filters as the list endpoint. Ordered newest first (id DESC).

Permission: ROLE_ADMIN

Query Parameters

Param Type Required Description
level string Exact PSR level: warning, error, critical, alert, emergency
search string Substring match on the message
from integer Unix timestamp lower bound (created_at >=)
to integer Unix timestamp upper bound (created_at <=)

Response 200

  • Content-Type: text/csv; charset=UTF-8
  • Content-Disposition: attachment; filename="logs-YYYYMMDD-HHMMSS.csv"
  • Streamed CSV with a UTF-8 BOM (Excel-friendly for Persian). Columns: id, level, message, context, channel, path, created_at. created_at is formatted as Y-m-d H:i:s.

DELETE /api/v1/admin/logs

Delete all persisted logs (truncate the app_log table). Irreversible.

Permission: ROLE_ADMIN

Response 200

{ "success": true, "data": { "deleted": 137 } }
  • deleted — number of rows removed.

Log Retention

Logs are pruned automatically based on the log_retention_days setting (see Site SettingsGET/PATCH /api/v1/admin/settings, whitelisted key log_retention_days, default 90, 0 = keep forever).

  • A daily scheduled task (App\Shared\Logging\Message\PruneLogsMessage, registered in src/Schedule.php, routed to scheduler_default) deletes logs older than log_retention_days.
  • Manual prune: php bin/console app:prune-logs (reads the same setting, deletes older-than-retention rows, prints the count).
  • Requires the scheduler worker: php bin/console messenger:consume scheduler_default.