The controller carried only IS_AUTHENTICATED_FULLY on the class and none of its 15 routes checked a permission. A secretary whose owner had turned `services` fully off could still create, rename and delete service categories, build item groups, replace group members, and rewrite service relations and per-branch overrides. Scope is intra-tenant privilege escalation, not IDOR: owned() and requireItem() already resolve every uuid against the caller's active environment, so no data crossed tenants. Gating is per-action (view/create/update/delete) and reuses denyServices() from ClinicServiceController in the same domain, so a secretary with `update` cannot create or delete. The call is the first statement in every action, before requireCategory/requireItem — placed after, an unknown uuid would answer 404 and leak whether the record exists. An earlier note claimed these endpoints were consumed by the booking flow and so could not be closed. That was wrong. service-selection/validate, the group routes and the relation routes have no consumer in any of the three API clients, and the sibling controller already puts every service read behind services.view — the booking modal reads service-items through it — so any flow needing services already needed the permission. The docs claimed appointment_settings.* for the includes routes, which was never enforced either; corrected to services.*. The test loops the whole route list rather than sampling, and a guard asserts the count of #[Route( equals the count of denyServices( so a future ungated route fails here. 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. منبعِ 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 |
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).