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` در صورت وجود برنامه حفظ میشود. تا وقتی حتی یک برنامه روشن و
|
> برمیگردد؛ `hours_of_work` در صورت وجود برنامه حفظ میشود. تا وقتی حتی یک برنامه روشن و
|
||||||
> دارای روز فعال باشد، همان مبنا قرار میگیرد.
|
> دارای روز فعال باشد، همان مبنا قرار میگیرد.
|
||||||
>
|
>
|
||||||
> چنین پزشکی (همه خاموش) در لیست عمومی `GET /api/v1/doctors` **نمایش داده میشود** — این
|
> چنین پزشکی (همه برنامهها خاموش، ولی فلگ `active_doctor_appointment` **روشن**) در لیست
|
||||||
> اندپوینت فیلتر `active` پیشفرض ندارد — ولی با `bookableRank` پایینتر از پزشکان دارای
|
> عمومی `GET /api/v1/doctors` **نمایش داده میشود** — چون هنوز فعال است — ولی با
|
||||||
> نوبت مرتب میشود و تنها با `active=1` از نتایج حذف میگردد. صفحهٔ تکی
|
> `bookableRank` پایینتر از پزشکان دارای نوبت مرتب میشود و تنها با `active=1` از نتایج حذف
|
||||||
> `GET /api/v1/doctor/{slug}` همیشه قابل دسترسی است.
|
> میگردد.
|
||||||
|
>
|
||||||
|
> **پزشک غیرفعال (`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` |
|
| `degree` | string | ❌ | `expert`, `general`, `specialist`, `subspecialistplus` |
|
||||||
| `name` | string | ❌ | جستجوی `LIKE` روی نام پزشک |
|
| `name` | string | ❌ | جستجوی `LIKE` روی نام پزشک |
|
||||||
| `sort` | string | ❌ | `ASC` یا `DESC` (پیشفرض `DESC`) — مرتبسازی ثانویه بر اساس `doctorRate` |
|
| `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,
|
'parent_id' => $s->getParent()?->getId() !== null ? (string) $s->getParent()->getId() : null,
|
||||||
], $this->specialties->toArray()),
|
], $this->specialties->toArray()),
|
||||||
'active' => $this->activeDoctorAppointment && $sf['has_schedule'],
|
'active' => $this->activeDoctorAppointment && $sf['has_schedule'],
|
||||||
|
// فلگ خام فعال/غیرفعال بودن پزشک (مستقل از داشتن برنامه) — کلاینت عمومی
|
||||||
|
// مثل nobat724 با این میتواند صفحهٔ پزشک غیرفعال را 404 کند.
|
||||||
|
'is_active' => $this->activeDoctorAppointment,
|
||||||
'img' => $this->images ?? [],
|
'img' => $this->images ?? [],
|
||||||
'social_media' => $this->socialMedia,
|
'social_media' => $this->socialMedia,
|
||||||
'expertise' => array_map(fn(DoctorService $ds) => [
|
'expertise' => array_map(fn(DoctorService $ds) => [
|
||||||
|
|||||||
@@ -108,6 +108,12 @@ class DoctorRepository extends ServiceEntityRepository
|
|||||||
// Legacy admin escape hatch: active=0 → flag explicitly off.
|
// Legacy admin escape hatch: active=0 → flag explicitly off.
|
||||||
$qb->andWhere('d.activeDoctorAppointment = false');
|
$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.
|
// Bookable doctors always rank above non-bookable ones.
|
||||||
|
|||||||
@@ -100,6 +100,24 @@ class DoctorBookingStateAggregationTest extends ApiTestCase
|
|||||||
$this->assertSame('نوبتدهی آنلاین غیرفعال است', $data['free_turn']);
|
$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
|
public function testDisabledClinicScheduleDoesNotMaskActivePersonalSchedule(): void
|
||||||
{
|
{
|
||||||
['doctor' => $doctor, 'personal' => $personal, 'clinic' => $clinicSchedule]
|
['doctor' => $doctor, 'personal' => $personal, 'clinic' => $clinicSchedule]
|
||||||
|
|||||||
@@ -71,17 +71,22 @@ class DoctorListBookableSortFilterTest extends ApiTestCase
|
|||||||
return $body['data'];
|
return $body['data'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDefaultListShowsAllDoctorsBookableFirst(): void
|
public function testDefaultListHidesDeactivatedDoctorsBookableFirst(): void
|
||||||
{
|
{
|
||||||
$items = $this->listDoctors();
|
$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->assertSame($this->bookable->getUuid(), $items[0]['uuid']);
|
||||||
$this->assertTrue($items[0]['active']);
|
$this->assertTrue($items[0]['active']);
|
||||||
|
|
||||||
$rest = array_column(array_slice($items, 1), 'uuid');
|
$rest = array_column(array_slice($items, 1), 'uuid');
|
||||||
sort($rest);
|
sort($rest);
|
||||||
$expected = [$this->noSchedule->getUuid(), $this->flagOff->getUuid(), $this->bookingDisabled->getUuid()];
|
$expected = [$this->noSchedule->getUuid(), $this->bookingDisabled->getUuid()];
|
||||||
sort($expected);
|
sort($expected);
|
||||||
$this->assertSame($expected, $rest);
|
$this->assertSame($expected, $rest);
|
||||||
foreach (array_slice($items, 1) as $item) {
|
foreach (array_slice($items, 1) as $item) {
|
||||||
|
|||||||
Reference in New Issue
Block a user