fix(appointment): one weekly schedule per doctor, place chosen per shift
A doctor working both at their own practice and at a clinic had to write two independent schedules and neither panel could see the other, so the clinic showed an empty form even though the doctor had configured their practice. The schedule is now a single record owned by the doctor. What varies between days is the place: the context of a shift is read from its location_id, not from the record it lives in. Booking in a context therefore sees only that context's days, so a personal-practice secretary still cannot book a clinic day. The caller's own context decides which addresses they may assign: the doctor gets every place of theirs, a clinic manager only its own, and shifts outside their reach are returned for display but preserved verbatim on save. Existing per-clinic rows are merged by migration; location_id was already stored on every shift, so no context information is lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Appointment;
|
||||
|
||||
use App\Appointment\Entity\WeeklySchedule;
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Doctor\Entity\DoctorAddress;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* پزشکی که هم مطب شخصی دارد هم کلینیک، یک برنامهٔ هفتگی دارد و نه دو تا.
|
||||
*
|
||||
* آنچه بین روزها فرق میکند مکان است: شنبه مطب، دوشنبه کلینیک. محیطِ هر روز از آدرسِ
|
||||
* همان شیفت خوانده میشود، پس منشیِ مطب شخصی روزِ کلینیک را نمیبیند و برعکس.
|
||||
*/
|
||||
class UnifiedDoctorScheduleTest extends ApiTestCase
|
||||
{
|
||||
/** @return array{0: Doctor, 1: Clinic, 2: \App\Auth\Entity\User, 3: DoctorAddress, 4: DoctorAddress} */
|
||||
private function makeDoctorWithBothPlaces(): array
|
||||
{
|
||||
$doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
||||
$doctor = new Doctor($doctorUser, 'دکتر دو-محیطی');
|
||||
$doctor->setMobileNumber($doctorUser->getMobileNumber());
|
||||
$this->em->persist($doctor);
|
||||
|
||||
$ownerUser = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
||||
$clinic = new Clinic($ownerUser);
|
||||
$clinic->setName('کلینیک تست');
|
||||
$clinic->getDoctors()->add($doctor);
|
||||
$this->em->persist($clinic);
|
||||
$this->em->flush();
|
||||
|
||||
$personal = DoctorAddress::forDoctor($doctor);
|
||||
$inClinic = DoctorAddress::forClinic($clinic->getId());
|
||||
$this->em->persist($personal);
|
||||
$this->em->persist($inClinic);
|
||||
$this->em->flush();
|
||||
|
||||
return [$doctor, $clinic, $ownerUser, $personal, $inClinic];
|
||||
}
|
||||
|
||||
/** @param array<int, array{location: int, start: string, end: string}> $days */
|
||||
private function settingOf(array $days): array
|
||||
{
|
||||
$setting = [];
|
||||
foreach ($days as $dayKey => $spec) {
|
||||
$setting[(string) $dayKey] = ['sessions' => [[
|
||||
'active' => true,
|
||||
'location_id' => $spec['location'],
|
||||
'start_time' => $spec['start'],
|
||||
'end_time' => $spec['end'],
|
||||
'duration_per_patient' => 30,
|
||||
'has_rest' => false,
|
||||
'patient_limit' => null,
|
||||
]]];
|
||||
}
|
||||
|
||||
return $setting;
|
||||
}
|
||||
|
||||
public function testDoctorSeesEveryPlaceOfTheirsAsSelectable(): void
|
||||
{
|
||||
[$doctor, , , $personal, $inClinic] = $this->makeDoctorWithBothPlaces();
|
||||
|
||||
$body = $this->authJson('GET', "/api/v1/appointment-settings/available-locations/{$doctor->getUuid()}", $doctor->getUser());
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
$ids = array_map(static fn(array $a): int => (int) $a['id'], $body['data']['data'] ?? []);
|
||||
|
||||
self::assertContains($personal->getId(), $ids);
|
||||
self::assertContains($inClinic->getId(), $ids, 'آدرس کلینیک هم باید برای خود پزشک انتخابشدنی باشد');
|
||||
}
|
||||
|
||||
public function testClinicOwnerOnlySeesClinicAddressesAsSelectable(): void
|
||||
{
|
||||
[$doctor, $clinic, $owner, $personal, $inClinic] = $this->makeDoctorWithBothPlaces();
|
||||
|
||||
$body = $this->authJson('GET', "/api/v1/appointment-settings/available-locations/{$doctor->getUuid()}?clinic_uuid={$clinic->getUuid()}", $owner);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
$ids = array_map(static fn(array $a): int => (int) $a['id'], $body['data']['data'] ?? []);
|
||||
|
||||
self::assertSame([$inClinic->getId()], $ids);
|
||||
self::assertNotContains($personal->getId(), $ids);
|
||||
}
|
||||
|
||||
public function testBothContextsReadTheSameRecord(): void
|
||||
{
|
||||
[$doctor, $clinic, , $personal] = $this->makeDoctorWithBothPlaces();
|
||||
|
||||
$this->em->persist($this->newWeeklySchedule($doctor, $this->settingOf([
|
||||
0 => ['location' => $personal->getId(), 'start' => '09:00', 'end' => '12:00'],
|
||||
])));
|
||||
$this->em->flush();
|
||||
|
||||
$personalView = $this->authJson('GET', "/api/v1/appointment-settings/weekly-schedule/{$doctor->getUuid()}", $doctor->getUser())['data']['data'];
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
$clinicView = $this->authJson('GET', "/api/v1/appointment-settings/weekly-schedule/{$doctor->getUuid()}?clinic_uuid={$clinic->getUuid()}", $doctor->getUser())['data']['data'];
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
self::assertSame($personalView['uuid'], $clinicView['uuid'], 'برنامه یکی است، از هر دو پنل همان دیده میشود');
|
||||
self::assertNotEmpty($personalView['locations'], 'پاسخ باید همهٔ مکانهای پزشک را برای برچسبزدن بدهد');
|
||||
}
|
||||
|
||||
public function testClinicOwnerCannotWipeThePersonalShift(): void
|
||||
{
|
||||
[$doctor, $clinic, $owner, $personal, $inClinic] = $this->makeDoctorWithBothPlaces();
|
||||
|
||||
$this->em->persist($this->newWeeklySchedule($doctor, $this->settingOf([
|
||||
0 => ['location' => $personal->getId(), 'start' => '09:00', 'end' => '12:00'],
|
||||
])));
|
||||
$this->em->flush();
|
||||
|
||||
// مدیر کلینیک شنبه را با شیفت کلینیک میفرستد و شیفت مطب را از ورودی حذف کرده.
|
||||
$this->authJson('PATCH', "/api/v1/appointment-settings/weekly-schedule/{$doctor->getUuid()}", $owner, [
|
||||
'clinic_uuid' => $clinic->getUuid(),
|
||||
'schedule' => $this->settingOf([
|
||||
0 => ['location' => $inClinic->getId(), 'start' => '16:00', 'end' => '20:00'],
|
||||
]),
|
||||
]);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
$this->em->clear();
|
||||
$reloaded = $this->em->getRepository(Doctor::class)->find($doctor->getId());
|
||||
$schedule = $this->em->getRepository(WeeklySchedule::class)->findUnified($reloaded);
|
||||
$ids = array_map(
|
||||
static fn(array $s): int => (int) $s['location_id'],
|
||||
$schedule->getSetting()['0']['sessions']
|
||||
);
|
||||
|
||||
self::assertContains($personal->getId(), $ids, 'شیفت مطب شخصی باید دستنخورده بماند');
|
||||
self::assertContains($inClinic->getId(), $ids);
|
||||
}
|
||||
|
||||
public function testEachContextOnlySeesItsOwnDays(): void
|
||||
{
|
||||
[$doctor, $clinic, , $personal, $inClinic] = $this->makeDoctorWithBothPlaces();
|
||||
|
||||
// همهٔ روزها را میسازیم تا نتیجه به روزِ هفتهٔ اجرای تست وابسته نباشد:
|
||||
// روزهای زوج مطب، روزهای فرد کلینیک.
|
||||
$days = [];
|
||||
foreach (range(0, 6) as $dayKey) {
|
||||
$days[$dayKey] = $dayKey % 2 === 0
|
||||
? ['location' => $personal->getId(), 'start' => '09:00', 'end' => '12:00']
|
||||
: ['location' => $inClinic->getId(), 'start' => '16:00', 'end' => '20:00'];
|
||||
}
|
||||
|
||||
$this->em->persist($this->newWeeklySchedule($doctor, $this->settingOf($days)));
|
||||
$this->em->flush();
|
||||
|
||||
$date = date('Y-m-d', strtotime('+3 days'));
|
||||
// شاخص روز در برنامه: ۰ شنبه است و date('w') یکشنبه را صفر میگیرد.
|
||||
$dayKey = ((int) date('w', strtotime($date)) + 1) % 7;
|
||||
$isOwn = $dayKey % 2 === 0;
|
||||
|
||||
$personalSessions = $this->authJson('GET', "/api/v1/appointment-slots?doctor_uuid={$doctor->getUuid()}&date={$date}", $doctor->getUser())['data']['sessions'];
|
||||
$clinicSessions = $this->authJson('GET', "/api/v1/appointment-slots?doctor_uuid={$doctor->getUuid()}&date={$date}&clinic_uuid={$clinic->getUuid()}", $doctor->getUser())['data']['sessions'];
|
||||
|
||||
if ($isOwn) {
|
||||
self::assertNotEmpty($personalSessions, 'روز مطب باید در محیط شخصی نوبت بدهد');
|
||||
self::assertSame([], $clinicSessions, 'روز مطب نباید در کلینیک نوبت بدهد');
|
||||
} else {
|
||||
self::assertSame([], $personalSessions, 'روز کلینیک نباید در مطب شخصی نوبت بدهد');
|
||||
self::assertNotEmpty($clinicSessions, 'روز کلینیک باید در محیط کلینیک نوبت بدهد');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user