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>
138 lines
5.2 KiB
PHP
138 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Doctor;
|
|
|
|
use App\Appointment\Entity\WeeklySchedule;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Doctor\Entity\DoctorAddress;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* The public doctor payload must aggregate booking state across every weekly
|
|
* schedule (personal + each clinic). A doctor whose personal schedule is empty
|
|
* but who is bookable at a clinic used to be reported as «نوبتدهی غیرفعال».
|
|
*/
|
|
class DoctorBookingStateAggregationTest extends ApiTestCase
|
|
{
|
|
/** پاسخ عمومی doctor به شکل {data:{data:{…}}} است. */
|
|
private function doctorPayload(): array
|
|
{
|
|
$json = json_decode($this->client->getResponse()->getContent(), true) ?? [];
|
|
|
|
return $json['data']['data'] ?? [];
|
|
}
|
|
|
|
private function week(array $session): array
|
|
{
|
|
$days = array_fill_keys(range(0, 6), ['sessions' => []]);
|
|
$days[0] = ['sessions' => [$session]];
|
|
|
|
return $days;
|
|
}
|
|
|
|
private function session(bool $active, int $locationId): array
|
|
{
|
|
return [
|
|
'active' => $active,
|
|
'location_id' => $locationId,
|
|
'start_time' => '09:00',
|
|
'end_time' => '13:00',
|
|
'duration_per_patient' => 20,
|
|
];
|
|
}
|
|
|
|
/** @return array{doctor: Doctor, personal: WeeklySchedule, clinic: WeeklySchedule} */
|
|
private function makeDoctorWithInactivePersonalAndClinic(): array
|
|
{
|
|
$doctor = new Doctor($this->createUser(['ROLE_USER', 'ROLE_DOCTOR']), 'دکتر تجمیع');
|
|
$this->em->persist($doctor);
|
|
|
|
$clinic = new Clinic($this->createUser(['ROLE_USER', 'ROLE_CLINIC']));
|
|
$clinic->setName('کلینیک تجمیع');
|
|
$clinic->getDoctors()->add($doctor);
|
|
$this->em->persist($clinic);
|
|
$this->em->flush();
|
|
|
|
$personalAddress = DoctorAddress::forDoctor($doctor);
|
|
$this->em->persist($personalAddress);
|
|
$clinicAddress = DoctorAddress::forClinic($clinic->getId());
|
|
$this->em->persist($clinicAddress);
|
|
$this->em->flush();
|
|
|
|
$personal = new WeeklySchedule($doctor, $this->week($this->session(false, $personalAddress->getId())));
|
|
$clinicSchedule = new WeeklySchedule(
|
|
$doctor,
|
|
$this->week($this->session(true, $clinicAddress->getId())),
|
|
$clinic
|
|
);
|
|
$this->em->persist($personal);
|
|
$this->em->persist($clinicSchedule);
|
|
$this->em->flush();
|
|
|
|
return ['doctor' => $doctor, 'personal' => $personal, 'clinic' => $clinicSchedule];
|
|
}
|
|
|
|
public function testClinicScheduleKeepsDoctorBookableDespiteEmptyPersonalSchedule(): void
|
|
{
|
|
['doctor' => $doctor] = $this->makeDoctorWithInactivePersonalAndClinic();
|
|
|
|
$this->client->request('GET', '/api/v1/doctor/' . $doctor->getUuid());
|
|
$data = $this->doctorPayload();
|
|
|
|
$this->assertTrue($data['active']);
|
|
$this->assertStringContainsString('شنبه', $data['free_turn']);
|
|
}
|
|
|
|
public function testAllSchedulesDisabledReportsBookingDisabled(): void
|
|
{
|
|
['doctor' => $doctor, 'personal' => $personal, 'clinic' => $clinicSchedule]
|
|
= $this->makeDoctorWithInactivePersonalAndClinic();
|
|
|
|
$personal->setMeta(['online_booking_enabled' => false]);
|
|
$clinicSchedule->setMeta(['online_booking_enabled' => false]);
|
|
$this->em->flush();
|
|
|
|
$this->client->request('GET', '/api/v1/doctor/' . $doctor->getUuid());
|
|
$data = $this->doctorPayload();
|
|
|
|
$this->assertFalse($data['active']);
|
|
$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]
|
|
= $this->makeDoctorWithInactivePersonalAndClinic();
|
|
|
|
// برعکسِ سناریوی اول: شخصی فعال، کلینیکی خاموش — ترتیب ردیفها نباید مهم باشد.
|
|
$personal->setSetting($this->week($this->session(true, 0)));
|
|
$clinicSchedule->setMeta(['online_booking_enabled' => false]);
|
|
$this->em->flush();
|
|
|
|
$this->client->request('GET', '/api/v1/doctor/' . $doctor->getUuid());
|
|
$data = $this->doctorPayload();
|
|
|
|
$this->assertTrue($data['active']);
|
|
$this->assertStringContainsString('شنبه', $data['free_turn']);
|
|
}
|
|
}
|