Files
clinicpro/.claude/prompt/fix-clinic-filter-by-address-location.md

6.5 KiB

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

پروژه

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 — نادرست):

$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 (مرجع — مکان واقعی):

#[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 در فیلترها باشد:

$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 ظاهر شد.