<div dir="rtl" markdown="1">

# افزودن شهر به لیست پزشکان + رفع سقف خاموش limit

## پروژه

`clinicpro` (backend)

**cross-repo:** خروجی این endpoint را `nobat724_front/app/sitemap.js` مصرف می‌کند.
پرامپت همتا (بعد از این اجرا شود): `nobat724_front/.claude/prompt/sitemap-simplify-with-city.md`

## زمینه

در ممیزی SEO سایت عمومی دو محدودیت این endpoint باعث دو باگ در sitemap شد:

۱. **پاسخ لیست پزشکان فیلد شهر ندارد.** سایت عمومی چند-دامنه‌ای است (۳۵ دامنهٔ شهری) و باید بداند هر پزشک به کدام دامنه تعلق دارد. چون شهر در پاسخ نیست، sitemap دامنهٔ اصلی مجبور است **۳۵ بار جداگانه** لیست را با `city_id` بگیرد و از کل کم کند تا بفهمد کدام پزشک شهر اختصاصی ندارد. تولید sitemap ریشه ~۱۳ ثانیه طول می‌کشد.

۲. **پارامتر `limit` بی‌صدا به ۵۰ سقف می‌خورد.** کلاینت `limit=500` می‌فرستد، پاسخ ۵۰ رکورد است و هیچ نشانه‌ای از سقف‌خوردن در پاسخ نیست. این باعث شد sitemap ماه‌ها روی ۵۰ پزشک بریده بماند (حلقهٔ صفحه‌بندی وقتی `items.length < limit` بود متوقف می‌شد). سمت فرانت با تکیه بر `meta.totalPages` رفع شد، ولی رفتار خاموشِ API همچنان تله است.

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

| فایل | نقش |
|------|-----|
| `clinicpro/src/Doctor/Entity/Doctor.php` | `toListArray()` — شکل پاسخ لیست |
| `clinicpro/src/Doctor/Repository/DoctorRepository.php` | سقف `limit` در خطوط ۴۵ و ۱۴۲ |
| `clinicpro/src/Doctor/Controller/DoctorController.php` | route `/api/v1/doctors` خط ۲۵۲ |
| `clinicpro/docs/api/doctor.md` | مستندات — الزاماً به‌روز شود |

## وضعیت فعلی

`src/Doctor/Entity/Doctor.php:540` — بدون شهر:

```php
public function toListArray(array $schedules = []): array
{
    $sf = $this->computeScheduleFields($schedules);
    return [
        'id'            => (string) $this->id,
        'uuid'          => $this->uuid,
        'name'          => $this->name,
        'gender'        => $this->gender,
        'degree'        => $this->degree,
        'img'           => $this->images ?? [],
        'specialties'   => array_map(fn(Specialty $s) => [...], $this->specialties->toArray()),
        'satisfaction'  => $this->hasPublicRating() ? (string) $this->doctorRatePercentage : null,
        'point'         => $this->hasPublicRating() ? (string) $this->doctorRate : null,
        'free_turn'     => $sf['free_turn'],
        'hours_of_work' => $sf['hours_of_work'],
        'active'        => $this->activeDoctorAppointment && $sf['has_schedule'],
        'owner_status'  => $this->ownerStatus,
    ];
}
```

`src/Doctor/Repository/DoctorRepository.php:45` و `:142` — سقف خاموش:

```php
$limit = min(50, max(1, (int) ($filters['limit'] ?? 10)));
```

> نکته: در پاسخ **جزئیات** پزشک (`toDetailArray`) شهر داخل `address[].city` هست، ولی `city` و `state` سطح‌بالا آرایهٔ خالی برمی‌گردند. سایت عمومی برای همین از `address[].city.id` استخراج می‌کند (`nobat724_front/lib/domainHelpers.js` → `extractEntityCityId`). این پرامپت آن رفتار را تغییر نمی‌دهد.

## وظایف

### ۱. افزودن شهر به `toListArray()`

شهرِ پزشک از آدرس‌هایش می‌آید. شهر **اولین آدرس** (یا آدرس اصلی، اگر مفهوم آدرس اصلی وجود دارد) به‌عنوان شهر پزشک برگردد — چون سایت عمومی هم برای canonical دقیقاً همین قاعده («یک شهر اصلی برای پزشک چند-شهری») را اعمال می‌کند.

```php
'city'  => $primaryAddress?->getCity() ? [
    'id'   => (string) $primaryAddress->getCity()->getId(),
    'name' => $primaryAddress->getCity()->getName(),
] : null,
'state' => $primaryAddress?->getProvince() ? [
    'id'   => (string) $primaryAddress->getProvince()->getId(),
    'name' => $primaryAddress->getProvince()->getName(),
] : null,
```

- شکل `{ id, name }` باشد تا با `city` در پاسخ لیست کلینیک‌ها یکسان باشد (آنجا آرایه‌ای از همین شکل است).
- `id` رشته باشد — هم‌راستا با بقیهٔ فیلدهای این متد.
- پزشک بدون آدرس → `null` (نه آرایهٔ خالی، تا با «شهر ندارد» تفکیک‌پذیر بماند).
- **N+1 نساز:** آدرس/شهر/استان در همان کوئری `findWithFilters` با `JOIN`/`addSelect` بارگذاری شود، نه lazy per-doctor.

### ۲. شفاف‌کردن سقف `limit`

سقف ۵۰ حفظ شود (محافظت از دیتابیس)، ولی دیگر خاموش نباشد:

- مقدار مؤثر `limit` در `meta` برگردد (اگر الان برنمی‌گردد) تا کلاینت بفهمد درخواستش کوتاه شده.
- در `docs/api/doctor.md` صریح نوشته شود: «`limit` حداکثر ۵۰؛ مقادیر بزرگ‌تر بی‌صدا به ۵۰ کاهش می‌یابند».

اگر تصمیم گرفتی سقف را برای مصرف‌کنندهٔ sitemap بالاتر ببری، آن را به‌صورت یک حد جداگانه و مستند انجام بده — نه با حذف `min()`.

### ۳. به‌روزرسانی مستندات

`clinicpro/docs/api/doctor.md` برای `GET /api/v1/doctors`:
- فیلدهای جدید `city` و `state` با مثال واقعی JSON
- رفتار و سقف `limit`

## نکات مهم

- `toListArray()` را مصرف‌کنندگان دیگری هم دارند (پنل ادمین، اپ Tauri). **فیلد اضافه می‌کنیم، فیلد موجود را تغییر نام یا حذف نمی‌کنیم** — تغییر افزایشی و backward-compatible باشد.
- بعد از تغییر، پاسخ واقعی را با یک پزشک دارای آدرس تست کن:
  `curl -s "https://clinic-pro.ir/api/v1/doctors?page=1&limit=5" | jq '.data.data[0] | {name, city, state}'`
- پزشک `beaca548-816f-4613-937d-360db01bd7c8` شهرش یاسوج (`city_id: 123`) است — نمونهٔ خوبی برای تأیید.
- Entity تغییر نمی‌کند (فقط متد سریال‌سازی) ⇒ migration لازم نیست.

</div>
