createUser(['ROLE_USER', 'ROLE_DOCTOR']); $doctor = new Doctor($user, 'دکتر منبع'); $doctor->setMobileNumber($user->getMobileNumber()); $this->em->persist($doctor); $this->em->flush(); $address = DoctorAddress::forDoctor($doctor); $address->setName('مطب شخصی'); $this->em->persist($address); $this->em->flush(); $type = new ResourceType('doctor', (int) $doctor->getId(), 'device', 'دستگاه'); $this->em->persist($type); $this->em->flush(); $resource = new ClinicResource($address, $type, 'لیزر CO2'); $resource->setSupervisor($doctor); $this->em->persist($resource); $this->em->flush(); return [$user, $doctor, $resource, $address]; } private function service(DoctorAddress $address, string $name, ?int $serviceMinutes): ServiceItem { $section = new ServiceSection($address->tenantEntityType(), $address->tenantEntityId(), 'بخش ' . $name); $this->em->persist($section); $item = new ServiceItem($section, $name, 5_000_000); $item->setBookable(true); if ($serviceMinutes !== null) { $item->setDurationMinutes($serviceMinutes); } $this->em->persist($item); $this->em->flush(); return $item; } private function offer(ClinicResource $resource, ServiceItem $item, ?int $minutes): void { $offering = new ResourceServiceOffering($resource, $item); $offering->setDurationMinutes($minutes); $this->em->persist($offering); $this->em->flush(); } /** @param array $extra */ private function body(ClinicResource $resource, int $start, array $extra = []): array { return array_merge([ 'resource_uuid' => $resource->getUuid(), 'slot_start' => $start, 'slot_end' => $start + 600, 'duration_from_services' => true, 'patient_mobile' => '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT), 'patient_name' => 'بیمار منبع', 'patient_national_code' => '00' . str_pad((string) random_int(0, 99_999_999), 8, '0', STR_PAD_LEFT), ], $extra); } private function reload(string $uuid): Appointment { $this->em->clear(); return $this->em->getRepository(Appointment::class)->findOneBy(['uuid' => $uuid]); } private function tomorrowAt(int $hour): int { return (int) strtotime('tomorrow midnight') + $hour * 3600; } // ── ✅ موفق ────────────────────────────────────────────────────────────── public function testTheAppointmentLandsOnTheResourceWithTheResourceDuration(): void { [$user, $doctor, $resource, $address] = $this->doctorWithResource(); // پیش‌فرض سرویس ۳۰ دقیقه است، ولی این دستگاه ۵۰ دقیقه می‌گیرد. $service = $this->service($address, 'RF فرکشنال', 30); $this->offer($resource, $service, 50); $start = $this->tomorrowAt(10); $res = $this->authJson('POST', '/api/v1/my/appointment', $user, $this->body($resource, $start, [ 'service_item_uuids' => [$service->getUuid()], ])); self::assertSame(201, $this->responseCode(), json_encode($res, JSON_UNESCAPED_UNICODE)); $appointment = $this->reload($res['data']['uuid']); self::assertSame($resource->getUuid(), $appointment->getResource()?->getUuid()); self::assertSame($start + 50 * 60, $appointment->getSlotEnd()); // پزشک فرستاده نشد؛ از ناظرِ منبع آمد. self::assertSame($doctor->getUuid(), $appointment->getDoctor()->getUuid()); } public function testSeveralServicesAddUpAndAPerRequestDurationWins(): void { [$user, , $resource, $address] = $this->doctorWithResource(); $laser = $this->service($address, 'لیزر زیربغل', 20); $cryo = $this->service($address, 'کرایوتراپی', 25); $this->offer($resource, $laser, 20); $this->offer($resource, $cryo, 25); $start = $this->tomorrowAt(11); $res = $this->authJson('POST', '/api/v1/my/appointment', $user, $this->body($resource, $start, [ 'service_item_uuids' => [$laser->getUuid(), $cryo->getUuid()], 'service_durations' => [$laser->getUuid() => 40], ])); self::assertSame(201, $this->responseCode(), json_encode($res, JSON_UNESCAPED_UNICODE)); self::assertSame($start + 65 * 60, $this->reload($res['data']['uuid'])->getSlotEnd()); } // ── ⚠️ مرزی ───────────────────────────────────────────────────────────── /** ظرفیت ۲ یعنی نوبت دوم هنوز جا دارد. */ public function testASecondBookingFitsWhileTheResourceStillHasCapacity(): void { [$user, , $resource, $address] = $this->doctorWithResource(); $resource->setCapacity(2); $this->em->flush(); $service = $this->service($address, 'اکسیژن‌تراپی', 60); $this->offer($resource, $service, 60); $start = $this->tomorrowAt(9); $this->em->persist(new ResourceOccupancy($resource, $start, $start + 3600)); $this->em->flush(); $this->authJson('POST', '/api/v1/my/appointment', $user, $this->body($resource, $start, [ 'service_item_uuids' => [$service->getUuid()], ])); self::assertSame(201, $this->responseCode()); } // ── ❌ خطا ─────────────────────────────────────────────────────────────── /** دستگاهی که موتور منبع‌محور گرفته، از پنل دوباره فروخته نمی‌شود. */ public function testATimeAlreadyTakenOnTheResourceIsRefused(): void { [$user, , $resource, $address] = $this->doctorWithResource(); $service = $this->service($address, 'مزوتراپی', 60); $this->offer($resource, $service, 60); $start = $this->tomorrowAt(14); $this->em->persist(new ResourceOccupancy($resource, $start, $start + 3600)); $this->em->flush(); $this->authJson('POST', '/api/v1/my/appointment', $user, $this->body($resource, $start, [ 'service_item_uuids' => [$service->getUuid()], ])); self::assertSame(409, $this->responseCode()); } public function testAServiceTheResourceDoesNotOfferIsRefused(): void { [$user, , $resource, $address] = $this->doctorWithResource(); $service = $this->service($address, 'ویزیت عمومی', 15); // بدون offering $this->authJson('POST', '/api/v1/my/appointment', $user, $this->body($resource, $this->tomorrowAt(12), [ 'service_item_uuids' => [$service->getUuid()], ])); self::assertSame(422, $this->responseCode()); } public function testAnUnknownResourceIsRefused(): void { [$user, , , $address] = $this->doctorWithResource(); $service = $this->service($address, 'لیزر پا', 20); $this->authJson('POST', '/api/v1/my/appointment', $user, [ 'resource_uuid' => 'no-such-resource', 'slot_start' => $this->tomorrowAt(13), 'slot_end' => $this->tomorrowAt(13) + 1200, 'duration_from_services' => true, 'service_item_uuids' => [$service->getUuid()], 'patient_mobile' => '09121234567', 'patient_name' => 'بیمار تست', 'patient_national_code' => '0012345678', ]); self::assertSame(422, $this->responseCode()); } /** منبعِ محیط دیگر روی نوبت این محیط نمی‌نشیند. */ public function testAResourceOfAnotherEnvironmentIsRefused(): void { [$user, , , $address] = $this->doctorWithResource(); [, , $foreignResource, $foreignAddress] = $this->doctorWithResource(); $service = $this->service($address, 'لیزر صورت', 30); $foreign = $this->service($foreignAddress, 'لیزر صورت', 30); $this->offer($foreignResource, $foreign, 30); $this->authJson('POST', '/api/v1/my/appointment', $user, $this->body($foreignResource, $this->tomorrowAt(15), [ 'doctor_uuid' => $this->em->getRepository(Doctor::class)->findByUser($user)->getUuid(), 'service_item_uuids' => [$service->getUuid()], ])); self::assertSame(422, $this->responseCode()); } }