createUser(['ROLE_USER', 'ROLE_DOCTOR']); $doctor = new Doctor($user, 'دکتر محل'); $doctor->setMobileNumber($user->getMobileNumber()); $this->em->persist($doctor); $this->em->flush(); return $doctor; } private function makeClinicWith(Doctor $doctor): Clinic { $clinic = new Clinic($this->createUser(['ROLE_USER', 'ROLE_CLINIC'])); $clinic->setName('کلینیک محل'); $clinic->getDoctors()->add($doctor); $this->em->persist($clinic); $this->em->flush(); return $clinic; } /** برنامه‌ای که همهٔ روزها یک شیفت روی $locationId دارد. */ private function scheduleFor(Doctor $doctor, ?Clinic $clinic, ?int $locationId): WeeklySchedule { $day = ['sessions' => [array_filter([ 'active' => true, 'location_id' => $locationId, 'start_time' => '09:00', 'end_time' => '13:00', 'duration_per_patient' => 20, ], fn($v) => $v !== null)]]; $schedule = new WeeklySchedule( $doctor, array_fill_keys(array_map('strval', range(0, 6)), $day), $clinic ); $this->em->persist($schedule); $this->em->flush(); return $schedule; } private function locations(Doctor $doctor, string $query = ''): array { $body = $this->authJson( 'GET', '/api/v1/appointment-booking-locations/' . $doctor->getUuid() . $query, $doctor->getUser() ); return $body['data']['booking_locations'] ?? []; } public function testLocationWithoutAnyAddressIsNotReturned(): void { $doctor = $this->makeDoctor(); $this->scheduleFor($doctor, null, null); self::assertSame([], $this->locations($doctor), 'محلی که آدرس ندارد نباید محل به حساب بیاید'); } public function testLocationWhoseShiftsPointOutsideItsContextIsNotReturned(): void { $doctor = $this->makeDoctor(); $clinic = $this->makeClinicWith($doctor); $clinicAddress = DoctorAddress::forClinic($clinic->getId()); $personal = DoctorAddress::forDoctor($doctor); $this->em->persist($clinicAddress); $this->em->persist($personal); $this->em->flush(); // برنامهٔ شخصی که شیفتش روی آدرس کلینیک نشسته — دقیقاً حالتی که در dev دیده شد. $this->scheduleFor($doctor, null, $clinicAddress->getId()); self::assertSame([], $this->locations($doctor)); } public function testValidLocationIsReturnedWithItsOpeningHours(): void { $doctor = $this->makeDoctor(); $address = DoctorAddress::forDoctor($doctor); $this->em->persist($address); $this->em->flush(); $this->scheduleFor($doctor, null, $address->getId()); $locations = $this->locations($doctor); self::assertCount(1, $locations); self::assertSame('personal', $locations[0]['type']); self::assertSame($address->getUuid(), $locations[0]['location_uuid']); self::assertCount(7, $locations[0]['opening_hours']); self::assertSame($address->getId(), $locations[0]['opening_hours'][0]['location_id']); } public function testDateParameterReportsPerDayAvailability(): void { $doctor = $this->makeDoctor(); $address = DoctorAddress::forDoctor($doctor); $this->em->persist($address); $this->em->flush(); // فقط شنبه (اندیس 0) شیفت فعال دارد. $active = ['sessions' => [[ 'active' => true, 'location_id' => $address->getId(), 'start_time' => '09:00', 'end_time' => '13:00', 'duration_per_patient' => 20, ]]]; $setting = array_fill_keys(array_map('strval', range(0, 6)), ['sessions' => []]); $setting['0'] = $active; $this->em->persist(new WeeklySchedule($doctor, $setting)); $this->em->flush(); $saturday = $this->nextWeekday(6); // date('w'): 6 = Saturday $sunday = $this->nextWeekday(0); $onSaturday = $this->locations($doctor, '?date=' . $saturday); $onSunday = $this->locations($doctor, '?date=' . $sunday); self::assertTrue($onSaturday[0]['available_on_date'] ?? null); self::assertFalse($onSunday[0]['available_on_date'] ?? null); } public function testInvalidDateIsRejected(): void { $doctor = $this->makeDoctor(); $this->authJson('GET', '/api/v1/appointment-booking-locations/' . $doctor->getUuid() . '?date=2026-13-99', $doctor->getUser()); self::assertSame(422, $this->responseCode()); } public function testEmptyReasonDistinguishesTheCauses(): void { $doctor = $this->makeDoctor(); $today = date('Y-m-d'); // بدون هیچ برنامه‌ای $body = $this->authJson('GET', "/api/v1/appointment-slots?doctor_uuid={$doctor->getUuid()}&date={$today}", $doctor->getUser()); self::assertSame('no_schedule', $body['data']['empty_reason'] ?? null); // برنامه هست ولی این روز شیفت ندارد $address = DoctorAddress::forDoctor($doctor); $this->em->persist($address); $this->em->flush(); $setting = array_fill_keys(array_map('strval', range(0, 6)), ['sessions' => []]); $this->em->persist(new WeeklySchedule($doctor, $setting)); $this->em->flush(); $body = $this->authJson('GET', "/api/v1/appointment-slots?doctor_uuid={$doctor->getUuid()}&date={$today}", $doctor->getUser()); self::assertSame('day_off', $body['data']['empty_reason'] ?? null); // تاریخ خارج از بازهٔ نوبت‌دهی $farFuture = date('Y-m-d', strtotime('+2 years')); $body = $this->authJson('GET', "/api/v1/appointment-slots?doctor_uuid={$doctor->getUuid()}&date={$farFuture}", $doctor->getUser()); self::assertSame('outside_window', $body['data']['empty_reason'] ?? null); } /** * اولین وقوعِ آن روز هفته در آینده. از فردا شروع می‌شود چون شیفت‌های امروز * ممکن است گذشته باشند و getAvailableSlots اسلات گذشته را برنمی‌گرداند — * وگرنه تست بسته به ساعت اجرا نتیجهٔ متفاوت می‌دهد. * * @param int $phpDow خروجی date('w') — 0=یکشنبه ... 6=شنبه */ private function nextWeekday(int $phpDow): string { for ($i = 1; $i <= 7; $i++) { $ts = strtotime("+{$i} day"); if ((int) date('w', $ts) === $phpDow) { return date('Y-m-d', $ts); } } return date('Y-m-d'); } }