A full role-by-role sweep (9 roles x 18 endpoints against the running app) showed
the addresses toggles in the owner's permission form controlled nothing. Grep
confirms it: no gate anywhere referenced 'addresses'. The panel's address list was
gated on appointment_settings.view instead — the same borrowed-permission pattern
already fixed for resources and treatment.
GET /api/v1/addresses now gates on addresses.view.
The resource drops to view-only. Creating, updating and deleting an address in
ClinicController is explicitly owner-or-admin
($clinic->getUser()->getId() !== $user->getId()), so those three actions could
never be delegated to a secretary or an invited doctor no matter what the form
said. Both role defaults narrow to ['view' => true] to match, and stored JSON
keeps its old keys harmlessly since merge only reads registry keys.
This widens secretary access: addresses.view defaults to true while
appointment_settings.view defaults to false, so secretaries who could not list
addresses now can. That is deliberate and costs no confidentiality — the same
addresses are already served anonymously from
GET /api/v1/clinic/{uuid}/addresses, which is whitelisted in security.yaml.
Verified live in three states: default 200, addresses.view off 403, and
addresses off with appointment_settings on still 403, proving the borrow is gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
32 KiB
Secretary API
Prefix:
/api/v1/secretary,/api/v1/secretaries
مدل Scope
هر رابطه منشی-پزشک دارای یک scope است که از تداخل بین محیطهای مختلف جلوگیری میکند:
| Scope | owner_type |
تعریفکننده | دسترسی |
|---|---|---|---|
| مطب شخصی | doctor |
خود پزشک | فقط نوبتها و دادههای مطب شخصی |
| کلینیک | clinic |
مدیر کلینیک | فقط نوبتها و دادههای کلینیک |
- یک منشی میتواند هم در مطب شخصی یک دکتر و هم در کلینیک همان دکتر فعال باشد (دو ردیف مجزا)
- منشی کلینیک میتواند به چند دکتر در همان کلینیک متصل باشد
- یک منشی میتواند به همان دکتر در چند کلینیک متفاوت تخصیص یابد (یک ردیف به ازای هر کلینیک). تا پیش از این، کلید یکتا فقط
(doctor_id, secretary_id, owner_type)بود و کلینیکِ دوم را تکراری میشمرد؛ حالا خودِ محیط هم بخشی از هویت رابطه است - scope فعال در runtime از جدول
user_active_contextخوانده میشود —db_uuidبههمراهdb_typeکه میگوید uuid مالِ پزشک است یا کلینیک - محدودسازی به پزشکانِ تخصیصیافته: منشیِ کلینیک فقط نوبتهای پزشکانی را میبیند/رزرو میکند که واقعاً به او تخصیص داده شدهاند — نه همهی پزشکان کلینیک. لیست نوبت (
GET /api/v1/my/appointments) باa.doctor IN (پزشکانِ تخصیصیافته)فیلتر میشود و گیت رزرو (POST /api/v1/my/appointment) رابطهی فعالِ همان (منشی، کلینیک، پزشک) را چک میکند. permission رزرو از همان ردیفِ پزشک خوانده میشود
Secretaries are linked to a doctor and have granular permissions controlling what they can do on behalf of the doctor.
POST /api/v1/secretary
Create a secretary for a doctor.
Permission: ROLE_DOCTOR (must own the doctor — creates owner_type='doctor') | ROLE_CLINIC (must have the doctor in its clinic — creates owner_type='clinic') | ROLE_ADMIN
Request Body (application/json)
{
"doctor_uuid": "550e8400-...",
"mobile_number": "09123456789",
"name": "سارا احمدی",
"national_code": "1234567890",
"address": "یزد، خیابان تست",
"password": "secretaryPass123",
"permissions": {
"version": 1,
"resources": {
"appointments": {
"view": true,
"create": true,
"cancel": false,
"update_status": true
},
"patients": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"payments": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"insurances": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"addresses": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"clinic_info": { "view": true, "update": false },
"inventory": {
"view": false,
"create": false,
"update": false,
"delete": false
},
"tags": {
"view": false,
"create": false,
"update": false,
"delete": false
},
"services": {
"view": false,
"create": false,
"update": false,
"delete": false
},
"staff": { "view": false, "create": false, "update": false, "delete": false },
"discounts": { "view": false, "create": false, "update": false, "delete": false },
"sms": { "view": false, "create": false, "update": false, "delete": false },
"appointment_settings": { "view": false, "update": false },
"clinic_doctors": { "view": false, "create": false, "update": false, "delete": false },
"subscription": { "view": false, "create": false }
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
doctor_uuid |
string (UUID) | ✅* | Single doctor to assign (legacy/doctor flow) |
doctor_uuids |
string[] (UUID) | ✅* | Clinic only — assign one secretary to several clinic doctors at once. When present (non-empty) and caller is ROLE_CLINIC, this multi-doctor path is used instead of doctor_uuid |
mobile_number |
string | ✅ | Secretary's login mobile. Persian/Arabic digits are accepted and normalized server-side — see README → Persian digit normalization |
name |
string | ❌ | Full name (نام + نام خانوادگی) → user_name |
national_code |
string | ❌ | کد ملی منشی (nullable). Persian/Arabic digits accepted and normalized |
address |
string | ❌ | آدرس منشی (nullable) |
password |
string | ❌ | Initial password (auto-generated if omitted) |
permissions |
object | ❌ | Permission set (see structure below) |
* یکی از doctor_uuid (تکپزشکی) یا doctor_uuids (چندپزشکیِ کلینیک) الزامی است.
پاسخِ حالت چندپزشکی (doctor_uuids + ROLE_CLINIC) — 201:
{
"success": true,
"data": {
"secretary_uuid": "550e8400-...",
"created": [ { "uuid": "...", "secretary_uuid": "...", "doctor_uuid": "...", "...": "..." } ],
"skipped_duplicate": [],
"skipped_limit": [],
"skipped_not_in_clinic": []
}
}
created: ردیفهای تازهساخته/فعالشده ·skipped_duplicate: قبلاً متصل بوده ·skipped_limit: سقفِ پلنِ آن پزشک پر است ·skipped_not_in_clinic: پزشک عضو کلینیک نیست. حلقه اتمیک است و بقیهی پزشکان ادامه مییابند.
Permissions Structure:
مجموعهٔ منابع را دیگر این فایل تعیین نمیکند: منبعِ واحد App\Shared\Security\PermissionCatalog است و از GET /api/v1/permission-catalog هم خوانده میشود — permission.md. فهرستِ فعلی: appointments, patients, treatment, payments, insurances, addresses, clinic_info, services, inventory, staff, tags, discounts, sms, appointment_settings, resources, clinic_doctors, subscription.
منبعِ subscription فقط view/create دارد؛ clinic_info, appointment_settings و treatment فقط view/update؛ و addresses فقط view (نوشتنِ آدرس owner-only است). منبعِ clinic_doctors فقط در حالت کلینیک معنا دارد (پزشک مستقل نه toggle نه منو) و در کاتالوگ با clinic_only: true علامت خورده.
mergePermissions هر منبع/اکشن ارسالشده را deep-merge میکند و هر دو شکلِ ورودی را میپذیرد: با envelope ({version, resources:{…}}) و نقشهٔ تخت ({patients:{…}}). تا پیش از این فقط شکلِ اول خوانده میشد و صفحهٔ ادمین که تخت میفرستد بیصدا بیاثر بود. منبع یا اکشنِ خارج از رجیستری بیصدا کنار گذاشته میشود؛ بقیهٔ کلیدهای همان درخواست اعمال میشوند.
پیشفرضها (DEFAULT_PERMISSIONS) سیاستِ نقشِ منشیاند، نه ساختار: appointments, patients, treatment, payments, insurances, addresses, clinic_info با view روشن؛ بقیه default-deny. منبعی که بعد از ساختِ یک ردیف به رجیستری اضافه شود، هنگام خواندن پیشفرضِ نقش را میگیرد نه false — پس نیازی به migration داده نیست.
اعمال (enforcement): همهٔ منابع در بکاند enforce میشوند، نه فقط appointments. منبعِ حقیقت، ستون JSON permission روی ردیفِ فعالِ DoctorSecretary در محیطِ فعالِ کاربر (UserActiveContext.db_uuid) است؛ نقطهٔ مرکزی App\Secretary\Security\SecretaryAccessChecker (can / canOrNonSecretary / denyUnlessGranted). نبودِ مجوز → 403 ERR_FORBIDDEN_001. نقشه:
| Resource | Enforced in | Action → endpoint |
|---|---|---|
appointments |
AppointmentAccessChecker, MyAppointmentsController, DashboardController |
view/create/cancel/update_status |
patients |
PatientController (خواندنها via scope() → بدون view هیچ پروندهای؛ افزودن/ویرایشِ زیرآیتمها = update؛ حذفِ یادداشت/سند/رکورد/تماس/پیام = delete — جدا از update) |
view/create/update/delete |
payments |
PaymentController::myPayments, PaymentMethodController (bank/pos), PatientController (کیفپول + پرداختِ جلسه) |
view/create/update/delete |
addresses |
AddressController::list (GET /api/v1/addresses). فقط view؛ نوشتنها owner-onlyاند. تا پیش از این این فهرست روی appointment_settings.view سوار بود و توگلِ آدرسها بیاثر بود |
view |
insurances |
InsuranceController (insurance-pricing, tenant-insurances, service-coverage, doctor-insurance) |
view/create/update/delete |
inventory |
InventoryController (items + packages) |
view/create/update/delete |
tags |
TenantTagController (لیست با tags.view یا patients.view؛ نوشتنها با tags.*) |
view/create/update/delete |
services |
ClinicServiceController (sections + items) و ServiceCatalogController (دستهٔ درختی، گروه انتخاب، روابط، override شعبهای — هر ۱۵ route، per-action؛ clinic-services.md). owner از محیطِ فعال با SecretaryAccessChecker::resolveOwnerEntity حل میشود چون EntityContextResolver منشی را نمیشناسد. گیتِ services.* پیش از گیتِ اشتراک اجرا میشود |
view/create/update/delete |
staff |
StaffController (resolveEntity منشیآگاه) |
view/create/update/delete |
discounts |
DiscountController (CRUD؛ suggestions جزو flowِ جلسه است و با discounts گِیت نمیشود) |
view/create/update/delete |
sms |
SmsWalletController (balance/charge/logs/settings). endpointهای admin (قالب/ارسال) همچنان ROLE_ADMIN |
view/create/update |
appointment_settings |
AppointmentSettingsController::denyDoctorAccess → SecretaryAccessChecker::canForDoctor (اسکوپِ پزشکِ تخصیصیافته + توگل). clinic_uuid برای محیطِ کلینیک لازم است |
view/update |
clinic_doctors (فقط کلینیک) |
ClinicController::detachDoctor (delete)، ClinicDoctorPermissionController (view/update)، ClinicInvitationController (create/view/update/delete) via SecretaryAccessChecker::canForClinic |
view/create/update/delete |
subscription |
SubscriptionController::my (view — پاسخِ کاهشیافته، نه ۴۰۳)، trial (create)، PaymentController::initiateSubscription (create) |
view/create |
استثنای subscription.view — پاسخِ کاهشیافته بهجای ۴۰۳: GET /api/v1/subscription/my عمداً ۴۰۳ نمیدهد. بدون این مجوز subscription و used_trial تهی برمیگردند و از effective_plan فقط features و max_secretaries و max_resources میماند؛ با مجوز، پلنِ کامل (uuid, name, level, active) و اشتراکِ فعال هم میآید. دلیلش این است که FeatureGate و useSubscription در همهٔ صفحات به سقفها و فلگهای قابلیت نیاز دارند؛ ۴۰۳ کل پنل را میشکست. اطلاعاتِ هویتی و مالیِ اشتراک پشت مجوز میماند.
نقشهای غیرمنشی (ROLE_CLINIC/ROLE_DOCTOR/ROLE_ADMIN) از این چک عبور میکنند (canOrNonSecretary برایشان true). منشیِ بدون رابطهٔ فعال/context هیچ مجوزی ندارد → همهچیز 403.
{
"version": 1,
"resources": {
"appointments": {
"view": true, // Can view appointments list
"create": true, // Can book appointments
"cancel": false, // Can cancel appointments
"update_status": true // Can mark as completed/no_show
},
"patients": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"payments": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"insurances": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"addresses": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"clinic_info": {
"view": true,
"update": false
},
"inventory": {
"view": false, // انبار: مشاهده
"create": false, // ایجاد کالا/بسته
"update": false,
"delete": false
},
"tags": {
"view": false, // تگها؛ لیست با tags.view یا patients.view
"create": false,
"update": false,
"delete": false
}
}
}
Response 201
{
"success": true,
"data": {
"uuid": "sec-uuid-...",
"user_name": "علی محمدی",
"mobile_number": "09123456789",
"doctor_name": "احمد رضایی",
"doctor_uuid": "...",
"owner_type": "doctor",
"clinic_uuid": null,
"is_active": true,
"national_code": "1234567890",
"address": "یزد، خیابان تست",
"permissions": { ... },
"created_at": 1717000000
}
}
مقادیر owner_type:
| مقدار | معنی |
|---|---|
doctor |
منشی توسط خود دکتر تعریف شده — فقط مطب شخصی |
clinic |
منشی توسط مدیر کلینیک تعریف شده — فقط کلینیک |
پیامک خوشآمد: پس از ساخت موفق منشی، یک پیامک بهصورت async به mobile_number منشی ارسال میشود (تگ secretary). متن از قالب ویرایشپذیر DB میآید (fallback به پیشفرض) و placeholderهای {owner} (نام دکتر یا کلینیک بسته به owner_type)، {username} (موبایل منشی) و {link} (لینک ورود) را جایگزین میکند. ویرایش متن از PATCH /api/v1/admin/sms/messages/secretary — رجوع به sms.md.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not the doctor owner / clinic owner / admin |
ERR_NOT_FOUND_001 |
404 | Doctor not found |
ERR_CONFLICT_001 |
409 | Secretary already added for this doctor in this same environment — همان منشی برای همان پزشک در کلینیکِ دیگر ۴۰۹ نمیگیرد |
ERR_SECRETARY_001 |
422 | Plan limit for secretaries reached |
GET /api/v1/secretary/{uuid}
Get secretary detail.
Permission: AUTH — must be the linked doctor or ROLE_ADMIN
Path Parameters
| Param | Type | Description |
|---|---|---|
uuid |
string (UUID) | Secretary UUID |
Response 200
{
"success": true,
"data": {
"uuid": "...",
"mobile_number": "09123456789",
"active": true,
"permissions": { ... },
"doctor": { "uuid": "...", "title": "علی احمدی" },
"created_at": 1717000000
}
}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_FORBIDDEN_001 |
403 | Not authorized |
ERR_NOT_FOUND_001 |
404 | Secretary not found |
PATCH /api/v1/secretary/{uuid}
Update secretary active status, profile fields (name/national_code/address), or permissions. تمام فیلدها اختیاریاند و فقط موارد ارسالشده اعمال میشوند.
Permission: ROLE_DOCTOR — must be the linked doctor
Request Body (application/json)
{
"active": false,
"name": "نام جدید",
"national_code": "9999999999",
"address": "آدرس جدید",
"permissions": {
"version": 1,
"resources": {
"appointments": {
"view": true,
"create": false,
"cancel": false,
"update_status": false
}
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
active |
boolean | ❌ | Enable/disable secretary |
name |
string | ❌ | بهروزرسانی نام کامل منشی (user_name) |
national_code |
string | ❌ | بهروزرسانی کد ملی (nullable) |
address |
string | ❌ | بهروزرسانی آدرس (nullable) |
permissions |
object | ❌ | New permissions object (deep-merged) |
Response 200
Updated secretary object.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_FORBIDDEN_001 |
403 | Not the linked doctor |
ERR_NOT_FOUND_001 |
404 | Secretary not found |
DELETE /api/v1/secretary/{uuid}
Delete a secretary.
Permission: ROLE_DOCTOR — must be the linked doctor
Response 200
{ "success": true, "data": { "message": "منشی حذف شد" } }
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_FORBIDDEN_001 |
403 | Not the linked doctor |
ERR_NOT_FOUND_001 |
404 | Secretary not found |
GET /api/v1/secretaries/{doctorUuid}
Get all secretaries for a specific doctor.
Permission: ROLE_DOCTOR (must own doctor) | ROLE_CLINIC (must have doctor in clinic) | ROLE_ADMIN
Path Parameters
| Param | Type | Description |
|---|---|---|
doctorUuid |
string (UUID) | Doctor UUID |
Response 200
{
"success": true,
"data": [
{
"uuid": "...",
"user_name": "علی محمدی",
"mobile_number": "09...",
"doctor_name": "احمد رضایی",
"doctor_uuid": "...",
"is_active": true,
"permissions": { ... },
"created_at": 1717000000
}
]
}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_FORBIDDEN_001 |
403 | Not authorized |
ERR_NOT_FOUND_001 |
404 | Doctor not found |
GET /api/v1/secretaries/clinic/{clinicUuid}
Get all secretaries across all doctors of a clinic.
Permission: ROLE_CLINIC (must own clinic) | ROLE_ADMIN
Path Parameters
| Param | Type | Description |
|---|---|---|
clinicUuid |
string (UUID) | Clinic UUID |
Response 200
{
"success": true,
"data": [
{
"uuid": "...",
"secretary_uuid": "...",
"user_name": "علی محمدی",
"mobile_number": "09...",
"doctor_name": "احمد رضایی",
"doctor_uuid": "...",
"is_active": true,
"permissions": { ... },
"created_at": 1717000000
}
]
}
Notes
- این endpoint فقط منشی های را برمیگرداند که با
owner_type='clinic'تعریف شدهاند - منشی های که خود دکتر (با
owner_type='doctor') تعریف کرده از این لیست مخفی هستند - یک منشی میتواند به چند دکتر در همان کلینیک متصل باشد — در لیست چندبار ظاهر میشود (یک ردیف به ازای هر دکتر). برای گروهبندی «یک منشی، چند پزشک» از
secretary_uuid(uuid کاربرِ منشی) استفاده کنید
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_FORBIDDEN_001 |
403 | Not clinic owner |
ERR_NOT_FOUND_001 |
404 | Clinic not found |
PUT /api/v1/secretaries/clinic/{clinicUuid}/doctors
همگامسازی مجموعهی پزشکانِ یک منشیِ کلینیک (owner_type='clinic'): پزشکانِ خواستهشده افزوده/فعال و بقیه غیرفعال میشوند. برای «افزودن/حذف پزشک از یک منشی موجود» بدون تغییر ساختاری.
ردیفهای تازهساختهشده
national_code،addressوpermissionsرا از ردیفهای موجودِ همان منشی کپی میکنند تا پروفایل یک شخص روی همهی پزشکانش یکسان بماند. اگر همراه با ویرایش پروفایل صدا زده میشود، اولPATCH /api/v1/secretary/{uuid}روی ردیفهای موجود و بعد این اندپوینت را فراخوانی کنید.
Permission: ROLE_CLINIC (must own clinic) | ROLE_ADMIN
Path Parameters
| Param | Type | Description |
|---|---|---|
clinicUuid |
string (UUID) | Clinic UUID |
Request Body (application/json)
{
"secretary_uuid": "550e8400-...",
"doctor_uuids": ["uuid-doc-a", "uuid-doc-b"]
}
| Field | Type | Required | Description |
|---|---|---|---|
secretary_uuid |
string (UUID) | ✅ | uuid کاربرِ منشی (همان secretary_uuid خروجی لیست/ساخت) |
doctor_uuids |
string[] (UUID) | ✅ | مجموعهی نهاییِ پزشکان؛ نبودها افزوده، اضافهها غیرفعال میشوند |
Response 200
{
"success": true,
"data": {
"added": 1,
"removed": 1,
"skipped_limit": [],
"skipped_not_in_clinic": []
}
}
| Field | Type | Description |
|---|---|---|
added |
int | تعداد ردیفهای افزوده/فعالشده |
removed |
int | تعداد ردیفهای غیرفعالشده |
skipped_limit |
string[] | uuid پزشکانی که به سقفِ پلن رسیدهاند (نادیده گرفته) |
skipped_not_in_clinic |
string[] | uuid پزشکانی که عضو این کلینیک نیستند |
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_006 |
403 | Not clinic owner nor admin |
ERR_VALIDATION_001 |
422 | secretary_uuid/doctor_uuids missing |
ERR_VALIDATION_002 |
404 | Clinic or secretary not found |
محدودیت پنل اشتراکی
تعداد منشیهای مجاز بر اساس پنل فعال doctor تعیین میشود:
| پنل | حداکثر منشی |
|---|---|
| Free (بدون اشتراک) | ۱ |
| Basic | ۳ |
| Professional | ۱۰ |
اگر تعداد منشیهای فعال به حد مجاز رسیده باشد، ایجاد منشی جدید خطای زیر را برمیگرداند:
{
"success": false,
"errors": [
{
"code": "ERR_SECRETARY_001",
"message": "پلن فعلی اجازه منشی بیشتر را نمیدهد"
}
]
}
برای افزایش محدودیت، باید پنل را از POST /api/v1/subscription/trial (تریال) یا POST /api/v1/subscription-payment (پرداخت) ارتقاء داد.
سهم منشی از نوبتهای آنلاین (درآمد و تسویه)
ادمین میتواند برای هر رابطهٔ منشی–پزشک/کلینیک، محاسبهٔ درآمد از نوبتهای آنلاین را فعال کند و درصد بدهد (admin.md). سهم از مبلغ خالص نوبت گرفته میشود: ابتدا هزینهٔ پنل پیامک، بعد مالیات، سپس درصدِ منشی روی «خالصِ پس از مالیات» — همان مبنایی که پورسانت نماینده از آن محاسبه میشود (settlement.md).
«آنلاین» یعنی چه؟ تقسیم مالی تنها پس از پرداخت موفق درگاه (PaymentManager) اجرا
میشود؛ نوبتی که در پنل ثبت و «قطعی» میشود از این مسیر عبور نمیکند و سهمی نمیسازد.
انتساب بر پایهٔ محیط نوبت است: کلینیکِ نوبت، وگرنه خودِ پزشک. اگر چند منشیِ سهمبر وجود
داشته باشد، هر کدام درصد خودش را میگیرد (تقسیم نمیشود)؛ اگر مجموع درصدها از ۱۰۰
بگذرد به نسبت کلیپ میشود و هشدار لاگ میگردد تا سهم سیستم منفی نشود.
سهم هر منشی در جدول secretary_earnings ثبت و بهصورت اعتبار در کیف پول همان کاربر
منظور میشود؛ برداشت از طریق POST /api/v1/settlement انجام میگیرد.
GET /api/v1/secretary/earnings/summary
خلاصهٔ درآمد منشیِ جاری.
Permission: AUTH (کاربر منشی)
Response 200
{
"success": true,
"data": {
"data": {
"enabled": true,
"share_percent": 5,
"relations": [
{ "relation_uuid": "rel-…", "doctor_name": "دکتر احمدی", "clinic_name": null, "share_percent": 5 }
],
"today_rials": 500000,
"this_month_rials": 3000000,
"total_rials": 9000000,
"appointments_count": 4,
"wallet_balance_rials": 9000000
}
}
}
| فیلد | توضیح |
|---|---|
enabled |
false یعنی هیچ رابطهٔ فعالی با سهمِ روشن ندارد؛ پنل پیام «فعال نیست» نشان میدهد (خطا نمیدهیم) |
share_percent |
درصد اولین رابطهٔ سهمبر؛ تفکیک کامل در relations |
today_rials |
از نیمهشب امروز |
this_month_rials |
۳۰ روز گذشته |
wallet_balance_rials |
موجودی کیف پول همان کاربر (مبنای تسویه) |
GET /api/v1/secretary/earnings/report
گزارش سطر-به-سطر سهم منشی (paginated).
Permission: AUTH (کاربر منشی)
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
page |
integer | ❌ | پیشفرض ۱ |
limit |
integer | ❌ | پیشفرض ۱۵، حداکثر ۱۰۰ |
from |
integer | ❌ | Unix — از تاریخ |
to |
integer | ❌ | Unix — تا تاریخ |
Response 200
{
"success": true,
"data": [
{
"uuid": "earning-uuid-…",
"appointment_uuid": "appt-uuid-…",
"doctor_name": "دکتر احمدی",
"gross_rials": 10000000,
"sms_fee_rials": 1000000,
"tax_rials": 818182,
"net_after_tax_rials": 8181818,
"share_percent": 5,
"share_rials": 409091,
"created_at": 1700000000
}
],
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}
منشیِ بدون سهم، پاسخ 200 با آرایهٔ خالی میگیرد (نه 403).
GET /api/v1/secretary/me
پروفایل منشیِ جاری: رابطهها با تنظیمات سهم + شمارههای شبا.
Permission: AUTH (کاربر منشی)
Response 200
{
"success": true,
"data": {
"data": {
"full_name": "زهرا رضایی",
"mobile": "0912…",
"bank_account": [
{ "id": "iban-uuid-…", "iban": "IR…", "bank_name": "ملی", "owner_name": null, "verified": false, "created_at": 1700000000 }
],
"relations": [
{ "relation_uuid": "rel-…", "doctor_name": "دکتر احمدی", "clinic_name": null, "online_share_enabled": true, "online_share_percent": 5 }
]
}
}
}
POST /api/v1/secretary/iban
افزودن شماره شبا (حداکثر ۲) به پروفایل کاربرِ منشی — مثل پنل نماینده.
Permission: AUTH (کاربر منشی)
Request Body (application/json)
{ "iban": "IR123456789012345678901234", "bank_name": "ملی", "owner_name": "زهرا رضایی" }
| Field | Type | Required | Description |
|---|---|---|---|
iban |
string | ✅ | الگوی IR + ۲۴ رقم (فاصلهها حذف میشود) |
bank_name |
string | ❌ | نام بانک |
owner_name |
string | ❌ | نام صاحب حساب |
Response 201
{ "success": true, "data": { "data": { "bank_account": [ { "id": "…", "iban": "IR…", "verified": false } ] } } }
verified همیشه false ثبت میشود؛ تأیید فقط از سمت ادمین انجام میگیرد و تسویه تنها با شبای تأییدشده مجاز است.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_VALIDATION_001 |
422 | شبا نامعتبر (field: iban) |
ERR_VALIDATION_001 |
422 | بیش از دو شبا (field: iban) |
DELETE /api/v1/secretary/iban/{id}
حذف یکی از شباهای منشیِ جاری.
Permission: AUTH (کاربر منشی)
Response 200
{ success, data: { data: { bank_account: [...] } } }
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_NOT_FOUND_001 |
404 | پروفایل/شبا یافت نشد |
کیف پول و تسویه اندپوینت اختصاصی ندارند:
GET /api/v1/wallet/balance،GET /api/v1/wallet/transactionsوPOST /api/v1/settlementکاربر-محورند (settlement.md).