fix(doctor): hide deactivated doctors from public site
The public list GET /api/v1/doctors only excluded inactive doctors when an explicit `active` filter was passed; with no param it returned everyone (deactivated doctors just ranked lower). Deactivated doctors (admin toggled active_doctor_appointment off) leaked onto nobat724. - DoctorRepository::findWithFilters: default (no `active` param) now filters activeDoctorAppointment = true. The active=1 (bookable) and active=0 (admin, inactive-only) escape hatches are unchanged. - Doctor::toDetailArray: expose raw `is_active` (= activeDoctorAppointment, independent of schedule) so public clients can 404 a deactivated doctor's profile page; distinct from `active` (flag && has_schedule). - Tests + docs/api/doctor.md updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-5
@@ -197,10 +197,18 @@ Get doctor detail for clinic owner — only doctors who are members of the authe
|
||||
> برمیگردد؛ `hours_of_work` در صورت وجود برنامه حفظ میشود. تا وقتی حتی یک برنامه روشن و
|
||||
> دارای روز فعال باشد، همان مبنا قرار میگیرد.
|
||||
>
|
||||
> چنین پزشکی (همه خاموش) در لیست عمومی `GET /api/v1/doctors` **نمایش داده میشود** — این
|
||||
> اندپوینت فیلتر `active` پیشفرض ندارد — ولی با `bookableRank` پایینتر از پزشکان دارای
|
||||
> نوبت مرتب میشود و تنها با `active=1` از نتایج حذف میگردد. صفحهٔ تکی
|
||||
> `GET /api/v1/doctor/{slug}` همیشه قابل دسترسی است.
|
||||
> چنین پزشکی (همه برنامهها خاموش، ولی فلگ `active_doctor_appointment` **روشن**) در لیست
|
||||
> عمومی `GET /api/v1/doctors` **نمایش داده میشود** — چون هنوز فعال است — ولی با
|
||||
> `bookableRank` پایینتر از پزشکان دارای نوبت مرتب میشود و تنها با `active=1` از نتایج حذف
|
||||
> میگردد.
|
||||
>
|
||||
> **پزشک غیرفعال (`active_doctor_appointment` خاموش):** با غیرفعالکردن پزشک از پنل ادمین،
|
||||
> او دیگر در لیست عمومی `GET /api/v1/doctors` ظاهر نمیشود (پیشفرض این اندپوینت پزشکان
|
||||
> غیرفعال را کنار میگذارد). صفحهٔ تکی `GET /api/v1/doctor/{slug}` همچنان پاسخ میدهد
|
||||
> (ادمین/کلینیک از همین اندپوینت برای مشاهده/ویرایش استفاده میکنند) و در بدنهٔ پاسخ فیلد
|
||||
> خام `is_active` (`= active_doctor_appointment`، مستقل از داشتن برنامه) را برمیگرداند؛
|
||||
> کلاینت عمومی مثل nobat724 با `is_active === false` صفحهٔ پزشک را `404` میکند. این با
|
||||
> فیلد `active` (که `active_doctor_appointment && has_schedule` است) فرق دارد.
|
||||
|
||||
---
|
||||
|
||||
@@ -224,7 +232,7 @@ List doctors with pagination and filters.
|
||||
| `degree` | string | ❌ | `expert`, `general`, `specialist`, `subspecialistplus` |
|
||||
| `name` | string | ❌ | جستجوی `LIKE` روی نام پزشک |
|
||||
| `sort` | string | ❌ | `ASC` یا `DESC` (پیشفرض `DESC`) — مرتبسازی ثانویه بر اساس `doctorRate` |
|
||||
| `active` | `0`/`1` | ❌ | `1` → فقط پزشکان **دارای نوبت** (تعریف پایین). `0` → فقط پزشکانی که فلگ `active_doctor_appointment` آنها خاموش است (کاربرد ادمین). بدون این پارامتر → **همه** پزشکان برمیگردند |
|
||||
| `active` | `0`/`1` | ❌ | `1` → فقط پزشکان **دارای نوبت** (تعریف پایین). `0` → فقط پزشکانی که فلگ `active_doctor_appointment` آنها خاموش است (کاربرد ادمین). بدون این پارامتر → فقط پزشکان **فعال** (`active_doctor_appointment` روشن)؛ پزشکان غیرفعال هرگز در لیست عمومی نمیآیند |
|
||||
|
||||
### مرتبسازی و تعریف «دارای نوبت»
|
||||
|
||||
|
||||
@@ -616,6 +616,9 @@ class Doctor
|
||||
'parent_id' => $s->getParent()?->getId() !== null ? (string) $s->getParent()->getId() : null,
|
||||
], $this->specialties->toArray()),
|
||||
'active' => $this->activeDoctorAppointment && $sf['has_schedule'],
|
||||
// فلگ خام فعال/غیرفعال بودن پزشک (مستقل از داشتن برنامه) — کلاینت عمومی
|
||||
// مثل nobat724 با این میتواند صفحهٔ پزشک غیرفعال را 404 کند.
|
||||
'is_active' => $this->activeDoctorAppointment,
|
||||
'img' => $this->images ?? [],
|
||||
'social_media' => $this->socialMedia,
|
||||
'expertise' => array_map(fn(DoctorService $ds) => [
|
||||
|
||||
@@ -108,6 +108,12 @@ class DoctorRepository extends ServiceEntityRepository
|
||||
// Legacy admin escape hatch: active=0 → flag explicitly off.
|
||||
$qb->andWhere('d.activeDoctorAppointment = false');
|
||||
}
|
||||
} else {
|
||||
// Public listing default: deactivated doctors (admin toggled the
|
||||
// active flag off) must never surface on the public site, even
|
||||
// without an explicit `active` filter. Bookability is a separate,
|
||||
// stricter concern handled by `active=1`.
|
||||
$qb->andWhere('d.activeDoctorAppointment = true');
|
||||
}
|
||||
|
||||
// Bookable doctors always rank above non-bookable ones.
|
||||
|
||||
@@ -100,6 +100,24 @@ class DoctorBookingStateAggregationTest extends ApiTestCase
|
||||
$this->assertSame('نوبتدهی آنلاین غیرفعال است', $data['free_turn']);
|
||||
}
|
||||
|
||||
public function testDetailExposesRawIsActiveFlagForDeactivatedDoctor(): void
|
||||
{
|
||||
['doctor' => $doctor] = $this->makeDoctorWithInactivePersonalAndClinic();
|
||||
|
||||
// فلگ روشن (پیشفرض): is_active باید true باشد.
|
||||
$this->client->request('GET', '/api/v1/doctor/' . $doctor->getUuid());
|
||||
$this->assertTrue($this->doctorPayload()['is_active']);
|
||||
|
||||
// ادمین پزشک را غیرفعال میکند → is_active=false (مبنای 404 در سایت عمومی).
|
||||
$doctor->setActiveDoctorAppointment(false);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', '/api/v1/doctor/' . $doctor->getUuid());
|
||||
$data = $this->doctorPayload();
|
||||
$this->assertFalse($data['is_active']);
|
||||
$this->assertFalse($data['active']);
|
||||
}
|
||||
|
||||
public function testDisabledClinicScheduleDoesNotMaskActivePersonalSchedule(): void
|
||||
{
|
||||
['doctor' => $doctor, 'personal' => $personal, 'clinic' => $clinicSchedule]
|
||||
|
||||
@@ -71,17 +71,22 @@ class DoctorListBookableSortFilterTest extends ApiTestCase
|
||||
return $body['data'];
|
||||
}
|
||||
|
||||
public function testDefaultListShowsAllDoctorsBookableFirst(): void
|
||||
public function testDefaultListHidesDeactivatedDoctorsBookableFirst(): void
|
||||
{
|
||||
$items = $this->listDoctors();
|
||||
|
||||
$this->assertCount(4, $items);
|
||||
// Deactivated doctor (active flag off) must be absent from the public
|
||||
// list even without an explicit `active` filter.
|
||||
$uuids = array_column($items, 'uuid');
|
||||
$this->assertNotContains($this->flagOff->getUuid(), $uuids, 'deactivated doctor must not appear');
|
||||
|
||||
$this->assertCount(3, $items);
|
||||
$this->assertSame($this->bookable->getUuid(), $items[0]['uuid']);
|
||||
$this->assertTrue($items[0]['active']);
|
||||
|
||||
$rest = array_column(array_slice($items, 1), 'uuid');
|
||||
sort($rest);
|
||||
$expected = [$this->noSchedule->getUuid(), $this->flagOff->getUuid(), $this->bookingDisabled->getUuid()];
|
||||
$expected = [$this->noSchedule->getUuid(), $this->bookingDisabled->getUuid()];
|
||||
sort($expected);
|
||||
$this->assertSame($expected, $rest);
|
||||
foreach (array_slice($items, 1) as $item) {
|
||||
|
||||
Reference in New Issue
Block a user