# نوبت‌دهی منبع‌محور در صفحهٔ نوبت‌ها — تب هر منبع + رزرو سرویسی + حذف تایم‌لاین منابع

> **وضعیت: انجام شد (۱۴۰۵/۰۵/۱۲).** کامیت‌ها: `3e3a2482` (تب منابع + فیلتر)،
> `fd27ceef` (مودال رزرو + حذف تایم‌لاین).
>
> ## این پرامپت در اجرا سه‌بار غلط از آب درآمد — چیزی که واقعاً شد:
>
> **۱. «نوبت بدون پزشک» پیاده شد و بعد کاملاً برگردانده شد.** `doctor` تهی‌پذیر شد و
> migration اجرا شد، ولی وسط کار معلوم شد مدل مجوز منشی روی سه‌تایی
> `(منشی، کلینیک، پزشک)` بنا شده و `DoctorSecretary.doctor` تهی‌پذیر نیست — یعنی برای
> نوبتِ بدون پزشک **هیچ ردیف مجوزی وجود ندارد**. تصمیم کاربر: منبع پزشکِ مسئول داشته
> باشد. migration رول‌بک و کد `git checkout` شد.
>
> **۲. «۷۳ فراخوانی getDoctor()» بیش‌برآورد بود.** بیشترشان روی entityهای دیگرند
> (`WeeklySchedule`، `Holiday`، `Rate`، `DoctorAddress`). عدد واقعی روی `Appointment`
> حدود ۲۵ بود، و diff سطح ۸ دقیقاً **۳۳ نقطهٔ جدید در ۱۶ فایل** داد.
>
> **۳. مهم‌ترین: کل موتور از قبل ساخته شده بود و این پرامپت از وجودش بی‌خبر بود.**
> `POST /api/v1/appointment-availability` (با `assignment` per اسلات)، `appointment-hold`،
> `appointment-confirm`، هوک `useResourceBooking.ts`، و حتی یک صفحهٔ کامل
> `ResourceBookingPage.tsx` روی `/admin/resource-booking`. `confirm` هم از قبل
> `doctor_uuid` می‌گیرد. پس **هیچ تغییر بک‌اندی برای رزرو لازم نبود** و تنها افزودنی
> بک‌اند، فیلتر `resource_uuid` روی فهرست نوبت‌ها شد.
>
> ## دو تلهٔ ابزاری که باید بدانی
>
> - **`phpstan` این پروژه تهی‌پذیری را چک نمی‌کند.** بررسی «صدا زدن متد روی تهی» سطح ۸
>   است و `phpstan.neon` روی سطح ۵. با یک خطای عمدی تست شد: `[OK] No errors`. برای این
>   جنس تغییر، گیت واقعی PHPUnit است، نه phpstan.
> - **`doctrine:migrations:diff` تغییر nullable را ندید** و به‌جایش یک migration بی‌ربط
>   `messenger_messages` ساخت. migration دستی نوشته شد.
>
> ## معماری‌ای که ماند
>
> - رزرو منبع از `hold → confirm` می‌رود، نه `POST /api/v1/appointment`. دلیلش حیاتی است:
>   فقط `HoldService` رکورد `resource_occupancy_buckets` می‌نویسد و قید یکتای
>   `uniq_bucket_resource_seat` تداخل را غیرممکن می‌کند. `book()` هیچ occupancy نمی‌نویسد،
>   پس رزرو منبع از آن مسیر بی‌گارد است.
> - موتور خدمت‌محور جواب می‌دهد؛ مودال نتیجه را به اسلات‌هایی تنگ می‌کند که `assignment`شان
>   همین منبع را دارد و آن نقش را به منبع قفل می‌کند.
>
> بقیهٔ این فایل متن اولیهٔ پرامپت است و برای تاریخچه نگه داشته شده — **به‌عنوان دستورالعمل
> اجرا معتبر نیست.**

---

## زمینه

صفحهٔ `/admin/appointments` امروز کاملاً پزشک‌محور است: تب‌ها فقط پزشک‌اند
(`AppointmentsPage.tsx:841`)، و منابع فقط یک نوار **فقط‌خواندنی** زیر زمان‌بندی دارند
(`AppointmentsPage.tsx:892-903`) که هیچ اقدامی روی آن ممکن نیست.

در مدل Resource-First، منبع واحد ظرفیت است: «لیزر CO2» و «لیزر NdYAG» سرویس‌های خودشان
(`ResourceServiceOffering`)، تقویم خودشان (`ResourceCalendar`) و استثناهای خودشان را دارند.
ولی کاربر نمی‌تواند برای آن‌ها نوبت ثبت کند، چون کل مسیر رزرو از پزشک عبور می‌کند.

## مشکل / هدف

**هدف:** هر منبع مثل پزشک تب خودش را داشته باشد؛ «افزودن نوبت» روی تب یک منبع، مودال
نوبت‌دهی **سرویسی** را برای همان منبع باز کند (لیست سرویس‌های همان منبع → انتخاب →
زمان خالی → بیمار → ثبت)؛ و نوار «منابع» زیر زمان‌بندی حذف شود.

**سه مانع واقعی در کد امروز:**

۱. **نوبت بدون پزشک ممکن نیست.** `Appointment::$doctor` با `nullable: false` تعریف شده
   (`Appointment.php:100-101`) و سازندهٔ entity هم `Doctor` می‌گیرد (`Appointment.php:256`).
   `book()` بدون `doctor_uuid` خطای ۴۲۲ می‌دهد (`AppointmentController.php:484`) و منبع فقط
   وقتی پزشک پیدا می‌کند که خودش پزشک باشد (`AppointmentController.php:471-473`). یعنی
   دستگاهِ بدون پزشک اصلاً قابل رزرو نیست.

۲. **اسلات سرویسی فقط پزشک‌محور است.** `GET /api/v1/appointment-service-slots` پزشک
   می‌خواهد و شرط می‌کند `booking_mode` همان پزشک `service` باشد
   (`AppointmentController.php:193-209`). منبع `WeeklySchedule` ندارد.

۳. تایم‌لاین منابع باید حذف شود.

**تصمیم گرفته‌شده (توسط کاربر):** مسیر «نوبت بدون پزشک» — `doctor` تهی‌پذیر شود.

### چرا این تصمیم آن‌قدر که به‌نظر می‌رسد پرریسک نیست (شواهد از کد)

- `Appointment::$resource` **از قبل وجود دارد** و تهی‌پذیر است (`Appointment.php:174-176`).
- تداخل منابع **از قبل در سطح دیتابیس** تضمین شده، نه در کد: `OccupancyBucket` با
  `UniqueConstraint('uniq_bucket_resource_seat', ['resource_id','bucket_at','seat'])`
  (`OccupancyBucket.php:22`). پس `activeSlotKey` مسئول تداخل **منبع** نیست.
- `activeSlotKey` فقط دوباره‌رزروی **همان پزشک** را می‌گیرد:
  `sprintf('%d:%d', $this->doctor->getId(), $this->slotStart)` (`Appointment.php:283`).
  وقتی پزشکی وجود ندارد، «دوباره‌رزروی پزشک» بی‌معناست و `null` بودنِ کلید معنای درستی
  است — نه یک حفرهٔ ایمنی.

**ریسک واقعی و باقی‌مانده:** ۷۳ فراخوانی `getDoctor()` در ۲۱+ فایل `src/` که همه امروز
`Doctor` غیرتهی فرض می‌کنند. این بخش سنگین کار است و باید تک‌تک بررسی شود.

## معیار پذیرش

- ✅ **موفق:** با توکن مالک کلینیک، `POST /api/v1/appointment` با بدنهٔ
  `{resource_uuid, service_item_uuids[], slot_start, patient_national_code, patient_gender}`
  و **بدون** `doctor_uuid`، برای منبعِ دستگاهی (`subject_kind = null`) → `200` و نوبت ذخیره
  می‌شود با `doctor_id = NULL` و `resource_id` پرشده. در UI: تب «لیزر CO2» → «افزودن نوبت» →
  انتخاب سرویس → انتخاب زمان → ثبت → نوبت در فهرست همان تب دیده می‌شود.
- ✅ **موفق:** `GET /api/v1/appointment-service-slots?resource_uuid=…&date=…&service_item_uuids[]=…`
  → `200` با همان شکل پاسخِ حالت پزشک (`start_times[]`, `total_duration_minutes`).
- ❌ **خطا:** رزرو منبعی که آن سرویس را ارائه نمی‌دهد → `422` با پیام
  «این منبع این سرویس را ارائه نمی‌دهد» و `field = resource_uuid` (این بررسی از قبل در
  `AppointmentController.php:533-540` هست و باید در مسیر بدون‌پزشک هم اجرا شود).
- ❌ **خطا:** رزرو منبعِ محیط دیگر → `422` «منبع یافت نشد» با `field = resource_uuid`.
- ❌ **خطا:** نه `doctor_uuid` و نه `resource_uuid` → `422` با envelope خطا.
- ⚠️ **مرزی:** دو رزروِ هم‌زمان روی یک منبع با ظرفیت ۱ در یک بازه → دومی باید با
  `409` رد شود (از قید یکتای `uniq_bucket_resource_seat`، نه از بررسی در کد).
- ⚠️ **مرزی:** منبعی که آن روز شیفت ندارد → `start_times` خالی و پیام «زمان خالی کافی
  نیست»، نه خطای ۵۰۰.
- ⚠️ **مرزی:** نوبت‌های قدیمیِ دارای پزشک باید بدون تغییر کار کنند (هم API، هم پنل، هم
  سایت عمومی) — `doctor` تهی‌پذیر شده، حذف نشده.

## فایل‌های مرتبط

| فایل | نقش |
|------|-----|
| `src/Appointment/Entity/Appointment.php` | `doctor` تهی‌پذیر، `activeSlotKey`، `toArray()` |
| `src/Appointment/Controller/AppointmentController.php` | `book()` و `serviceSlots()` |
| `src/Appointment/Booking/Entity/OccupancyBucket.php` | تضمین یکتاییِ اشغال منبع (فقط مرجع — تغییر نمی‌کند) |
| `src/Resource/Entity/ResourceServiceOffering.php` | سرویس‌ها و مدت مؤثرِ هر منبع |
| `src/Resource/Entity/ResourceCalendar.php` | شیفت هفتگی منبع (مبنای اسلات) |
| `migrations/` | migration تهی‌پذیر کردن `appointments.doctor_id` |
| `assets/admin/pages/AppointmentsPage.tsx` | تب‌ها، مودال ثبت، حذف بخش منابع |
| `assets/admin/components/appointments/DoctorTabs.tsx` | تب‌ها (باید عمومی شود) |
| `assets/admin/components/appointments/ServiceSlotPicker.tsx` | انتخاب سرویس/زمان (باید منبع را هم بپذیرد) |
| `assets/admin/components/appointments/ResourceTimeline.tsx` + `.test.tsx` | **حذف** |
| `assets/admin/hooks/useResourceTimeline.ts` | **حذف** اگر مصرف‌کنندهٔ دیگری ندارد |
| `docs/api/appointment-booking.md`, `docs/api/appointment.md`, `docs/api/resource.md` | مستندات |

## وضعیت فعلی

`Appointment.php` — پزشک اجباری و کلید یکتا بر پایهٔ پزشک:

```php
#[ORM\ManyToOne(targetEntity: Doctor::class)]
#[ORM\JoinColumn(name: 'doctor_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')]
private Doctor $doctor;

public function __construct(Doctor $doctor, User $user, int $slotStart, int $slotEnd)

private function refreshActiveSlotKey(): void
{
    $this->activeSlotKey = !$this->isReserve && in_array($this->status, self::SLOT_OCCUPYING_STATUSES, true)
        ? sprintf('%d:%d', $this->doctor->getId(), $this->slotStart)
        : null;
}

public function getDoctor(): Doctor { return $this->doctor; }
```

`AppointmentController::book()` — بدون پزشک رد می‌شود:

```php
if ($doctorUuid === '' && $resource->subject() instanceof \App\Doctor\Entity\Doctor) {
    $doctorUuid = $resource->subject()->getUuid();
}
…
if ($doctorUuid === '' || $slotStart <= 0 || (!$hasServices && $slotEnd <= $slotStart)) {
    return $this->error(ErrorCodes::ERR_VALIDATION_001, 'doctor_uuid یا resource_uuid به‌همراه slot_start الزامی است', 422);
}

$doctor = $this->doctorRepo->findByUuid($doctorUuid);
if ($doctor === null) {
    return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
}
```

`AppointmentController::serviceSlots()` — پزشک‌محور و مقیّد به `booking_mode` پزشک:

```php
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
if ($doctor === null) {
    return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
}
…
$mode = ($schedule ? $schedule->getMeta() : WeeklySchedule::DEFAULT_META)['booking_mode'] ?? WeeklySchedule::MODE_SLOT;
if ($mode !== WeeklySchedule::MODE_SERVICE) {
    return $this->error(ErrorCodes::ERR_VALIDATION_001, 'این پزشک در حالت نوبت‌دهی سرویسی نیست', 422);
}
```

`AppointmentsPage.tsx` — تب فقط پزشک، و بخش منابع که باید حذف شود:

```tsx
{showDoctorTabs && (
  <DoctorTabs doctors={doctors} selected={selectedDoctorUuid} onSelect={setSelectedDoctorUuid} showAll={isAdmin} />
)}
…
{/* منابعِ قابل رزرو، زیر همان روز — یک نوبت می‌تواند هم‌زمان اتاق و
    دستگاه را بگیرد و آن است که ظرفیت را تمام می‌کند. */}
<div style={{ marginTop: 'var(--gap)', paddingTop: 'var(--gap)', borderTop: '1px solid var(--border)' }}>
  <h2 className="section-title" style={{ margin: '0 0 12px', fontSize: 15 }}>منابع</h2>
  <ResourceTimeline
    lanes={resourceDay?.resources ?? []}
    dayStart={resourceDay?.date ?? 0}
    loading={resourceTimelineLoading}
    error={resourceTimelineError}
  />
</div>
```

## وظایف

> ترتیب اجباری است: بک‌اند اول. وظیفهٔ ۱ پایهٔ بقیه است و اگر ناقص بماند، بقیه روی
> خرابه ساخته می‌شوند.

### ۱. تهی‌پذیر کردن `Appointment::$doctor`

```php
#[ORM\ManyToOne(targetEntity: Doctor::class)]
#[ORM\JoinColumn(name: 'doctor_id', referencedColumnName: 'id', nullable: true, onDelete: 'RESTRICT')]
private ?Doctor $doctor = null;

public function __construct(?Doctor $doctor, User $user, int $slotStart, int $slotEnd)

public function getDoctor(): ?Doctor { return $this->doctor; }

/**
 * کلید یکتای اسلات فقط دوباره‌رزروی «همان پزشک» را می‌گیرد. نوبتِ منبع‌محورِ بدون
 * پزشک چنین تداخلی ندارد؛ تداخلِ خودِ منبع را قید یکتای
 * `uniq_bucket_resource_seat` روی `resource_occupancy_buckets` می‌گیرد.
 */
private function refreshActiveSlotKey(): void
{
    $this->activeSlotKey = $this->doctor !== null
        && !$this->isReserve
        && in_array($this->status, self::SLOT_OCCUPYING_STATUSES, true)
            ? sprintf('%d:%d', $this->doctor->getId(), $this->slotStart)
            : null;
}
```

سپس **همهٔ ۷۳ فراخوانی `getDoctor()`** را بررسی کن:

```bash
ddev exec grep -rn "getDoctor()" src/ | wc -l     # باید ۷۳ باشد
ddev exec php vendor/bin/phpstan analyse          # سطح ۵ — تهی‌پذیرهای بررسی‌نشده را می‌گیرد
```

قاعده: هر جا پزشک واقعاً لازم است (تعرفه، برنامهٔ هفتگی، پیامک پزشک) → `null` را با
خطای معنادار رد کن؛ هر جا فقط نمایشی است (`toArray()` خط ۴۹۹ و ۵۰۳) → مقدار تهی برگردان
نه استثنا.

**نحوه تست:** `ddev exec php bin/phpunit tests/Appointment/` باید کامل سبز بماند (رگرسیون
مسیر پزشک‌دار). به‌علاوه `ddev exec php vendor/bin/phpstan analyse` بدون خطای تهی‌پذیری.

### ۲. Migration

```bash
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
```

**نحوه تست:** بعد از migrate، `DESCRIBE appointments;` باید `doctor_id` را `YES` (nullable)
نشان دهد، و ردیف‌های موجود دست‌نخورده بمانند:
```bash
ddev exec mysql -e "SELECT COUNT(*) FROM appointments WHERE doctor_id IS NULL;"   # قبل از فیچر: 0
```

### ۳. اسلات سرویسیِ منبع‌محور

`GET /api/v1/appointment-service-slots` را طوری توسعه بده که **یا** `doctor_uuid` بگیرد
**یا** `resource_uuid` — با همان شکل پاسخ.

**الگو: Strategy.** یک interface مثل `ServiceSlotSource` با دو پیاده‌سازی
`DoctorServiceSlotSource` و `ResourceServiceSlotSource`. دلیل انتخاب (guidelines §۵):
همین حالا **دو** پیاده‌سازی واقعی وجود دارد — پزشک از `WeeklySchedule` و اسلات‌های پزشک
می‌آید، منبع از `ResourceCalendar` + `ResourceException` + `resource_blocks` + اشغال.
بدون Strategy، این می‌شد یک `if` روی نوعِ شناسه داخل کنترلر که با هر منبعِ جدید رشد می‌کند.

نکات محاسبه برای منبع:
- مدت هر سرویس از `ResourceServiceOffering` همان منبع (مدت مؤثر)، نه از پیش‌فرض سرویس.
- شرط `booking_mode === MODE_SERVICE` برای منبع **اعمال نشود** — آن قید مالِ برنامهٔ
  هفتگی پزشک است و منبع اصلاً `WeeklySchedule` ندارد.
- بازه‌های اشغال از همان مسیری بیاید که تایم‌لاین منبع می‌خواند
  (`ResourceOccupancyRepository`)، نه یک کوئری موازیِ جدید.

**نحوه تست:**
```bash
TOKEN=$(curl -sk -X POST https://clinic-pro.ddev.site/api/v1/user/login \
  -H 'Content-Type: application/json' \
  -d '{"mobile_number":"0912000201","password":"QaTest@1234"}' | jq -r .access_token)

curl -sk "https://clinic-pro.ddev.site/api/v1/appointment-service-slots?resource_uuid=<UUID>&date=$(date +%F)&management=1&service_item_uuids[]=<SERVICE_UUID>" \
  -H "Authorization: Bearer $TOKEN" | jq
# انتظار: success:true و start_times آرایه‌ای از {start,end,start_time}
```

### ۴. رزرو بدون پزشک در `book()`

- اگر `resource_uuid` آمده و منبع پزشک‌پشت ندارد، دیگر به `doctor_uuid` اصرار نکن.
- شرط خطا به این تغییر کند: «حداقل یکی از `doctor_uuid` یا `resource_uuid` لازم است».
- بررسی «این منبع این سرویس را ارائه نمی‌دهد» (`AppointmentController.php:533-540`) باید
  در مسیر بدون‌پزشک هم اجرا شود — امروز داخل بلوکی است که به `$duration` وابسته است.
- بررسی مالکیت محیط منبع (`EntityContext::forBooking`) وقتی پزشک نداریم باید از **محیط
  کاربر جاری** بیاید، نه از پزشک.

**نحوه تست:** رزرو واقعی روی یک منبع دستگاهی (بدون `doctor_uuid`) → ۲۰۰؛ سپس همان بازه
دوباره → ۴۰۹. هر دو با `curl` و توکن بالا. و
`ddev exec mysql -e "SELECT doctor_id, resource_id FROM appointments ORDER BY id DESC LIMIT 1;"`.

### ۵. تب منابع در کنار تب پزشکان

`DoctorTabs.tsx` امروز فقط `{uuid, name}` می‌گیرد و رفتارش کاملاً عمومی است — **همان را
عمومی کن** (مثلاً `EntityTabs`) به‌جای ساختن کامپوننت دوم؛ اسم فعلی‌اش تنها چیزِ پزشکیِ
آن است (guidelines §۵: اول بگرد، بعد توسعه بده، در آخر بساز).

تب انتخاب‌شده باید در URL بنشیند (`useUrlState`) وگرنه «بازگشت» نما را می‌پراند —
همان قاعده‌ای که در `CLAUDE.md` برای وضعیت لیست آمده. پیشنهاد: `?tab=doctor:<uuid>` و
`?tab=resource:<uuid>` تا یک کلید هر دو نوع را بگیرد.

**نحوه تست:** `npx vitest run assets/admin/pages/AppointmentsPage.test.tsx` + تست جدید:
کلیک روی تب یک منبع → فهرست همان منبع؛ رفرش صفحه → همان تب فعال بماند.

### ۶. مودال ثبت نوبت برای منبع

`ServiceSlotPicker` باید به‌جای `doctorUuid` اجباری، یکی از این دو را بگیرد. سرویس‌ها
برای منبع از `GET /api/v1/resource/{uuid}/services` می‌آید (نه از
`appointment-booking-services/{doctorUuid}`).

ترتیب مودال دقیقاً مثل عکس مرجع و مثل حالت پزشک بماند:
سرویس‌های منبع → سرویس‌های انتخاب‌شده (با مدت قابل ویرایش) → زمان‌های خالی → جستجوی
بیمار → هزینه → ثبت.

**نحوه تست:** `npx vitest run assets/admin/components/appointments/ServiceSlotPicker.test.tsx`
(تست موجود نباید بشکند) + تست جدید برای حالت منبع. سناریوی UI: تب «لیزر CO2» → افزودن
نوبت → یک سرویس → یک زمان → کد ملی بیمار → ثبت → نوبت در فهرست ظاهر شود.

### ۷. حذف تایم‌لاین منابع

- بلوک `<h2>منابع</h2> + <ResourceTimeline …>` از `AppointmentsPage.tsx` حذف شود.
- `ResourceTimeline.tsx` و `ResourceTimeline.test.tsx` حذف شوند.
- `useResourceTimeline.ts` **فقط اگر** مصرف‌کنندهٔ دیگری ندارد حذف شود:
  ```bash
  grep -rn "useResourceTimeline" assets/admin/
  ```
- `GET /api/v1/resources/timeline` در بک‌اند **دست‌نخورده** بماند (اندپوینت خودش
  مشکلی ندارد؛ فقط این مصرف‌کننده حذف می‌شود). اگر بعد از حذف هیچ کلاینتی ندارد، در
  گزارش پایانی ذکر کن تا کاربر تصمیم بگیرد.

**نحوه تست:** `npx vitest run` کامل سبز؛ و اسکرین‌شات صفحه بعد از
`ddev exec yarn dev` که دیگر بخش «منابع» زیر زمان‌بندی ندارد.

### ۸. مستندات

- `docs/api/appointment-booking.md`: `resource_uuid` بدون `doctor_uuid`، و اینکه
  `doctor` در پاسخ می‌تواند `null` باشد.
- `docs/api/appointment.md`: پارامتر `resource_uuid` در `appointment-service-slots` با
  JSON واقعیِ اجرا (نه دست‌ساز).
- `docs/api/resource.md`: اشاره به اینکه سرویس‌های منبع مبنای رزرو منبع‌محورند.

## نکات مهم

- **این تغییر cross-repo است.** `nobat724_front` و `clinic-pro-tauri` هر دو کلاینت همین
  API‌اند و `appointment.doctor` را غیرتهی فرض می‌کنند. تغییر قرارداد در build آن‌ها خطا
  **نمی‌دهد** و در رانتایم می‌شکند (guidelines §۳). بعد از وظیفهٔ ۱، مصرف واقعی را در
  `nobat724_front/services/response.js` و صفحات نوبت دستی دنبال کن و نتیجه را گزارش بده.
- `activeSlotKey` را برای نوبت بدون پزشک `null` بگذار — این حفره نیست؛ تداخل منبع را
  `uniq_bucket_resource_seat` می‌گیرد. اگر وسوسه شدی کلید را `r<id>:<slot>` کنی، اول
  بررسی کن که با ظرفیت >۱ منبع نمی‌شکند (منبع سه‌ظرفیتی سه رزرو هم‌زمان دارد).
- Controller نازک بماند: منطق انتخاب اسلات در Service/Strategy، کوئری در Repository،
  تزریق با constructor injection.
- خطاها با `AppException(ErrorCodes::ERR_XXX)` و پیام فارسی؛ پاسخ‌ها با
  `$this->success()` / `$this->error()`.
- تاریخ‌ها Unix timestamp صحیح؛ رشته‌های UI فارسی و تاریخ‌های نمایشی جلالی.
- اگر وسط کار معلوم شد یکی از ۷۳ فراخوانی `getDoctor()` نیازمند تصمیم محصولی است
  (مثلاً «سهم منشی از نوبت بدون پزشک چطور حساب شود؟»)، **متوقف شو و بپرس** — حدس نزن.
