# فیلتر کلینیک بر اساس شهر/استانِ آدرس کلینیک

## پروژه

`clinicpro` (Backend). تغییر فقط منطق داخلی است؛ قرارداد API (`GET /api/v1/clinics?city=&state=`) و مصرف `nobat724_front` تغییر نمی‌کند — نیازی به پرامپت همتای فرانت نیست.

## زمینه

معماری مکانِ کلینیک عوض شده: قبلاً شهر/استان روی خودِ `Clinic` (ستون‌های `city_id`/`province_id`) ذخیره می‌شد، اما حالا این ستون‌ها عملاً پر نمی‌شوند و **شهر/استان واقعی روی آدرس کلینیک** است — یعنی رکورد `DoctorAddress` با `clinic_id = clinic.id`، که `city_id`/`province_id` خودش را دارد.

شواهد DB:
- `clinics`: تنها رکوردها `city_id`/`province_id` ندارند (یا قدیمی‌اند). فیلتر روی `c.cityId`/`c.provinceId` هیچ نتیجه‌ای نمی‌دهد.
- `doctor_addresses` با `type='clinic'` و `clinic_id` پر: مثلاً `clinic_id=226 → city_id=132 (یزد), province_id=100`، `clinic_id=225 → city_id=101 (تبریز), province_id=1`.

در نتیجه `GET /api/v1/clinics?city=132&state=100` (یزد) صفر کلینیک برمی‌گرداند، هرچند کلینیک ۲۲۶ در یزد است.

## مشکل / هدف

`ClinicRepository::findWithFilters` باید فیلتر `city`/`state` را از طریق **آدرس کلینیک** (`DoctorAddress` با `clinic_id = clinic.id`) اعمال کند، نه ستون‌های منسوخِ `c.cityId`/`c.provinceId`.

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

| فایل | نقش |
|------|-----|
| `src/Clinic/Repository/ClinicRepository.php` | `findWithFilters` — فیلتر مکان |
| `src/Doctor/Entity/DoctorAddress.php` | حامل مکان: `clinicId` (scalar)، `city` (ManyToOne City)، `province` (ManyToOne Province) |
| `src/Clinic/Entity/Clinic.php` | `cityId`/`provinceId` منسوخ (دیگر مبنای فیلتر نیست) |

## وضعیت فعلی (کد واقعی)

`ClinicRepository::findWithFilters` (مکان روی خود Clinic — نادرست):
```php
$qb = $this->createQueryBuilder('c')
    ->leftJoin('c.specialties', 's')
    ->distinct();

if (!empty($filters['state'])) {
    $qb->andWhere('c.provinceId = :state')->setParameter('state', (int) $filters['state']);
}
if (!empty($filters['city'])) {
    $qb->andWhere('c.cityId = :city')->setParameter('city', (int) $filters['city']);
}
if (!empty($filters['specialty'])) {
    $qb->andWhere('s.id = :specialty')->setParameter('specialty', (int) $filters['specialty']);
}
$qb->orderBy('c.id', $sort);
```

`DoctorAddress` (مرجع — مکان واقعی):
```php
#[ORM\Column(name: 'clinic_id', type: 'integer', nullable: true)]
private ?int $clinicId = null;            // scalar (نه رابطه‌ی Doctrine به Clinic)

#[ORM\ManyToOne(targetEntity: City::class)]
#[ORM\JoinColumn(name: 'city_id', ...)]
private ?City $city = null;

#[ORM\ManyToOne(targetEntity: Province::class)]
#[ORM\JoinColumn(name: 'province_id', ...)]
private ?Province $province = null;
```
> چون رابطه‌ی Doctrine بین `Clinic` و `DoctorAddress` وجود ندارد (فقط scalar `clinicId`)، باید با یک join شرط‌دار (`WITH addr.clinicId = c.id`) join کرد.

## وظایف

### ۱. join به آدرس کلینیک و فیلتر روی city/province آدرس
`c.provinceId`/`c.cityId` را با join به `DoctorAddress` (روی `clinic_id`) و فیلتر `addr.city`/`addr.province` جایگزین کن. join فقط وقتی لازم است که `city` یا `state` در فیلترها باشد:

```php
$qb = $this->createQueryBuilder('c')
    ->leftJoin('c.specialties', 's')
    ->distinct();

if (!empty($filters['city']) || !empty($filters['state'])) {
    // DoctorAddress رابطه‌ی Doctrine به Clinic ندارد؛ با شرط clinicId join می‌کنیم
    $qb->join(
        \App\Doctor\Entity\DoctorAddress::class,
        'addr',
        \Doctrine\ORM\Query\Expr\Join::WITH,
        'addr.clinicId = c.id'
    );

    if (!empty($filters['city'])) {
        $qb->andWhere('IDENTITY(addr.city) = :city')
           ->setParameter('city', (int) $filters['city']);
    }
    if (!empty($filters['state'])) {
        $qb->andWhere('IDENTITY(addr.province) = :state')
           ->setParameter('state', (int) $filters['state']);
    }
}

if (!empty($filters['specialty'])) {
    $qb->andWhere('s.id = :specialty')->setParameter('specialty', (int) $filters['specialty']);
}
$qb->orderBy('c.id', $sort);
```
- `IDENTITY(addr.city)` کلید خارجی (city_id) را بدون join اضافی به جدول `categories` می‌گیرد — سبک و دقیق.
- `distinct()` حفظ شود (یک کلینیک ممکن است چند آدرس داشته باشد).
- `Paginator` موجود روی همین `$qb` کار می‌کند؛ به‌خاطر join شرط‌دار، `distinct` باید فعال بماند تا شمارش total درست باشد.

### ۲. مستندسازی `docs/api/clinic.md`
در بخش `GET /api/v1/clinics`، یک جمله اضافه کن که فیلتر `city`/`state` بر اساس **شهر/استانِ آدرس کلینیک** اعمال می‌شود (نه فیلد مستقیم روی کلینیک).

## نکات مهم

- این رفع، باگِ `Class Clinic has no field stateId` (که قبلاً به `provinceId` تغییر یافت) را هم به‌طور ریشه‌ای حل می‌کند: مکان دیگر از خود Clinic خوانده نمی‌شود.
- `DoctorAddress.clinicId` یک ستون scalar است (نه association)، پس join باید با `Join::WITH 'addr.clinicId = c.id'` باشد، نه `c.addresses`.
- ستون‌های `c.cityId`/`c.provinceId` را دست نزن (ممکن است جای دیگری استفاده شوند)؛ فقط مبنای فیلتر را عوض کن.
- پاسخ‌ها از `BaseController` (`paginated`)؛ migration لازم نیست.
- تست رفتاری (از داخل container و هاست):
  - `GET /api/v1/clinics?city=132&state=100` (یزد) → باید کلینیک ۲۲۶ را برگرداند (قبلاً صفر بود).
  - `GET /api/v1/clinics?city=101&state=1` (تبریز) → کلینیک ۲۲۵.
  - `GET /api/v1/clinics` بدون فیلتر → همه‌ی کلینیک‌ها (join اعمال نشود).
  - `?specialty=<id>` همچنان کار کند.
  - حتماً در محیط **prod** هم تست کن (`cache:clear --env=prod` سپس درخواست از `https://clinic-pro.ddev.site`) چون باگ قبلی فقط در prod ظاهر شد.
