Files
clinicpro/docs/api/secretary.md
T
hamedandClaude Opus 5 d6de746938 fix(permissions): apply the patient and tag read gates to invited clinic doctors
PatientController::resolveScope and TenantTagController::guardTagView only ever
checked the secretary, while every write in both controllers already ran through
both checkers. So an invited clinic doctor with patients.view off got 200 with an
empty list where a secretary got 403 — one permission, two behaviours. No data
was exposed either way; tenant scoping emptied the result.

The fix is not canOrNonMember. That collapses two different situations: a
membership row switched to active=false means the collaboration ended, and
ClinicDoctorPermission::can() returns false for everything in that case too.
Routing it through the permission gate turned the existing 404 on a single record
into a 403, which confirms the record exists to someone who just lost access.
ClinicRecordAccessTest caught it.

isActiveMemberDenied() answers the narrower question — active member, permission
off — and leaves a deactivated row to the data scope, which closes it with a 404
and discloses nothing. A test now pins that distinction so it cannot be collapsed
again.

Tags keep the tags.view OR patients.view rule, now for both roles.

Verified live in three states: active with both off 403/403, deactivated not 403,
active with patients.view on 200/200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:30:52 +03:30

32 KiB
Raw Blame History

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::denyDoctorAccessSecretaryAccessChecker::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).