Service-booking mode now selects services by section like slot mode: appointment-booking-services returns service_section per item; ServiceSlotPicker groups by section (SearchableSelect), accumulates picks across sections into a removable 'section -> service' chip list. Secretaries can override a service's duration for a single appointment without changing the service default: appointment-service-slots accepts durations[uuid] and both create endpoints accept service_durations; the override drives total duration and slot_end. Online (patient) booking is unaffected — it never sends overrides. Backend + frontend tests and docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
42 KiB
Admin API
Prefix:
/api/v1/admin
Permission: ALL endpoints in this file requireROLE_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 theirtenant_service_coverages) is purged in the same request — polymorphicentity_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 },
"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 مدتِ هر سرویس برای همان نوبت؛ پیشفرضِ سرویس در تنظیمات تغییر نمیکند.
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) |
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شماره پیگیری،atunix).
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}/approveor/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_numberfalls back to the linked user's mobile when the representation's ownmobile_numbercolumn is empty.
Deactivation, not deletion: the admin panel deactivates a representation via
PATCH /api/v1/representation/{uuid}with{ "active": false }rather than callingDELETE.
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_endUnix 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.
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 0–100 dimensions). Each row's
overallis the mean of the five dimensions (0–100) andscoreis that mean on a 0–5 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}/approveor/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-otp)؛nullاگر ارسال نشده باشد (مثلاً رد بهدلیل نبود الگو در سیاست 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"= inactivecommission_percent— integer string,0–100- 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 gatewaysmellat_enabled/sep_enabled—"1"= gateway selectable,"0"= gateway disabled (hidden from the payment selection list and rejected atinitiate). Unset = enabled (default).payment_allowed_frontend_hosts— comma-separated hosts allowed as a paymentfrontend_address(origin site to return to). Falls back to theALLOWED_FRONTEND_HOSTSenv 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=1to 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
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:
contextis a JSON string (ornull); aThrowablein the context is stored as a compactClass: message @ file:linestring, never the raw object.created_atis 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-8Content-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_atis formatted asY-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 Settings — GET/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 insrc/Schedule.php, routed toscheduler_default) deletes logs older thanlog_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.