# 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`)

```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](README.md#persian-digit-normalization-global) |
| `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`:**

```json
{
  "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](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](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`.

```json
{
    "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`

```json
{
  "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](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`

```json
{
  "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`)

```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`

```json
{ "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`

```json
{
  "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`

```json
{
  "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`)

```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`

```json
{
  "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       | ۱۰          |

اگر تعداد منشی‌های فعال به حد مجاز رسیده باشد، ایجاد منشی جدید خطای زیر را برمی‌گرداند:

```json
{
    "success": false,
    "errors": [
        {
            "code": "ERR_SECRETARY_001",
            "message": "پلن فعلی اجازه منشی بیشتر را نمی‌دهد"
        }
    ]
}
```

برای افزایش محدودیت، باید پنل را از `POST /api/v1/subscription/trial` (تریال) یا `POST /api/v1/subscription-payment` (پرداخت) ارتقاء داد.

---

## سهم منشی از نوبت‌های آنلاین (درآمد و تسویه)

ادمین می‌تواند برای هر رابطهٔ منشی–پزشک/کلینیک، محاسبهٔ درآمد از نوبت‌های آنلاین را
فعال کند و درصد بدهد ([admin.md](admin.md#put-apiv1adminsecretaryuuidonline-share)).
سهم از **مبلغ خالص** نوبت گرفته می‌شود: ابتدا هزینهٔ پنل پیامک، بعد مالیات، سپس درصدِ
منشی روی «خالصِ پس از مالیات» — همان مبنایی که پورسانت نماینده از آن محاسبه می‌شود
([settlement.md](settlement.md)).

**«آنلاین» یعنی چه؟** تقسیم مالی تنها پس از پرداخت موفق درگاه (`PaymentManager`) اجرا
می‌شود؛ نوبتی که در پنل ثبت و «قطعی» می‌شود از این مسیر عبور نمی‌کند و سهمی نمی‌سازد.
انتساب بر پایهٔ محیط نوبت است: کلینیکِ نوبت، وگرنه خودِ پزشک. اگر چند منشیِ سهم‌بر وجود
داشته باشد، **هر کدام درصد خودش** را می‌گیرد (تقسیم نمی‌شود)؛ اگر مجموع درصدها از ۱۰۰
بگذرد به نسبت کلیپ می‌شود و هشدار لاگ می‌گردد تا سهم سیستم منفی نشود.

سهم هر منشی در جدول `secretary_earnings` ثبت و به‌صورت اعتبار در کیف پول همان کاربر
منظور می‌شود؛ برداشت از طریق `POST /api/v1/settlement` انجام می‌گیرد.

---

### GET `/api/v1/secretary/earnings/summary`

خلاصهٔ درآمد منشیِ جاری.

**Permission:** `AUTH` (کاربر منشی)

#### Response `200`
```json
{
  "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`
```json
{
  "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`
```json
{
  "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`)
```json
{ "iban": "IR123456789012345678901234", "bank_name": "ملی", "owner_name": "زهرا رضایی" }
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `iban` | string | ✅ | الگوی `IR` + ۲۴ رقم (فاصله‌ها حذف می‌شود) |
| `bank_name` | string | ❌ | نام بانک |
| `owner_name` | string | ❌ | نام صاحب حساب |

#### Response `201`
```json
{ "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](settlement.md)).
