diff --git a/docs/api/doctor.md b/docs/api/doctor.md index f3662f3b..24813dfe 100644 --- a/docs/api/doctor.md +++ b/docs/api/doctor.md @@ -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` روشن)؛ پزشکان غیرفعال هرگز در لیست عمومی نمی‌آیند | ### مرتب‌سازی و تعریف «دارای نوبت» diff --git a/src/Doctor/Entity/Doctor.php b/src/Doctor/Entity/Doctor.php index 3a57300d..db7c1ff3 100644 --- a/src/Doctor/Entity/Doctor.php +++ b/src/Doctor/Entity/Doctor.php @@ -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) => [ diff --git a/src/Doctor/Repository/DoctorRepository.php b/src/Doctor/Repository/DoctorRepository.php index bcc312ea..e631693c 100644 --- a/src/Doctor/Repository/DoctorRepository.php +++ b/src/Doctor/Repository/DoctorRepository.php @@ -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. diff --git a/tests/Doctor/DoctorBookingStateAggregationTest.php b/tests/Doctor/DoctorBookingStateAggregationTest.php index 522d858a..2f1efe5e 100644 --- a/tests/Doctor/DoctorBookingStateAggregationTest.php +++ b/tests/Doctor/DoctorBookingStateAggregationTest.php @@ -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] diff --git a/tests/Doctor/DoctorListBookableSortFilterTest.php b/tests/Doctor/DoctorListBookableSortFilterTest.php index 6b6b7346..3cef449c 100644 --- a/tests/Doctor/DoctorListBookableSortFilterTest.php +++ b/tests/Doctor/DoctorListBookableSortFilterTest.php @@ -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) {