createUser(['ROLE_USER', 'ROLE_CLINIC']); $clinic = new Clinic($owner); $clinic->setName('کلینیک رزرو منبعی'); $this->em->persist($clinic); $this->em->flush(); $doctor = new Doctor($this->createUser(['ROLE_USER', 'ROLE_DOCTOR']), 'مینا یوسفی'); $this->em->persist($doctor); // بدون عضویت، رزرو در محیط کلینیک «محل نوبت‌دهی یافت نشد» می‌گیرد. $clinic->getDoctors()->add($doctor); $address = DoctorAddress::forClinic($clinic->getId()); $address->setName('شعبهٔ مرکزی'); $this->em->persist($address); $section = new ServiceSection('clinic', (int) $clinic->getId(), 'لیزر'); $this->em->persist($section); $this->em->flush(); return [$clinic, $doctor, $address, $section]; } private function resource(Clinic $clinic, DoctorAddress $address, string $name, ?Doctor $doctor = null): ClinicResource { $type = new ResourceType('clinic', (int) $clinic->getId(), 'r_' . bin2hex(random_bytes(3)), 'منبع'); $this->em->persist($type); $this->em->flush(); $resource = new ClinicResource($address, $type, $name); if ($doctor !== null) { $resource->linkTo($doctor); } $this->em->persist($resource); $this->em->flush(); return $resource; } private function service(ServiceSection $section, string $name, int $minutes = 30): ServiceItem { $item = new ServiceItem($section, $name, 8_000_000); $item->setDurationMinutes($minutes)->setBookable(true); $this->em->persist($item); $this->em->flush(); return $item; } /** @param array $extra */ private function book(array $extra): array { $start = time() + 86_400 + random_int(1, 5_000) * 60; return [$this->authJson('POST', '/api/v1/appointment', $this->createUser(['ROLE_USER']), $extra + [ 'slot_start' => $start, 'slot_end' => $start + 30 * 60, 'for_self' => true, 'patient_national_code' => str_pad((string) random_int(0, 9_999_999_999), 10, '0', STR_PAD_LEFT), 'patient_gender' => 'female', ]), $start]; } // ── ✅ موفق ────────────────────────────────────────────────────────────── public function testBookingForADoctorResourceInfersTheDoctor(): void { [$clinic, $doctor, $address] = $this->clinic(); $resource = $this->resource($clinic, $address, 'دکتر یوسفی', $doctor); // بدون `doctor_uuid` — پزشک از خودِ منبع درمی‌آید. [$res] = $this->book(['resource_uuid' => $resource->getUuid()]); self::assertSame(201, $this->responseCode(), json_encode($res, JSON_UNESCAPED_UNICODE)); $row = $res['data']['data']; self::assertSame($resource->getUuid(), $row['resource']['uuid']); self::assertSame('دکتر یوسفی', $row['resource']['name']); } public function testTheOldDoctorOnlyPathIsUntouched(): void { [, $doctor] = $this->clinic(); [$res] = $this->book(['doctor_uuid' => $doctor->getUuid()]); self::assertSame(201, $this->responseCode(), json_encode($res, JSON_UNESCAPED_UNICODE)); // سایت عمومی امروز `resource_uuid` نمی‌فرستد؛ پاسخ باید null بدهد نه خطا. self::assertNull($res['data']['data']['resource']); } // ── ❌ خطا ─────────────────────────────────────────────────────────────── public function testAResourceThatDoesNotOfferTheServiceIsRejected(): void { [$clinic, $doctor, $address, $section] = $this->clinic(); $offering = $this->resource($clinic, $address, 'دستگاه مجاز'); $other = $this->resource($clinic, $address, 'دستگاه بی‌ارتباط'); $service = $this->service($section, 'لیزر پا'); // فقط یکی از دو دستگاه این سرویس را می‌دهد. $this->em->persist(new ResourceServiceOffering($offering, $service)); $this->em->flush(); [$res] = $this->book([ 'doctor_uuid' => $doctor->getUuid(), 'clinic_uuid' => $clinic->getUuid(), 'resource_uuid' => $other->getUuid(), 'service_item_uuids' => [$service->getUuid()], ]); self::assertSame(422, $this->responseCode(), json_encode($res, JSON_UNESCAPED_UNICODE)); self::assertSame('این منبع این سرویس را ارائه نمی‌دهد', $res['errors'][0]['message']); } public function testAnUnknownResourceIsRejected(): void { $this->book(['resource_uuid' => '00000000-0000-4000-8000-000000000000']); self::assertSame(422, $this->responseCode()); } public function testNeitherDoctorNorResourceIsRejected(): void { $this->book([]); self::assertSame(422, $this->responseCode()); } // ── ⚠️ مرزی ────────────────────────────────────────────────────────────── public function testWithoutAnyOfferingRowsTheResourceIsNotSecondGuessed(): void { [$clinic, $doctor, $address, $section] = $this->clinic(); $resource = $this->resource($clinic, $address, 'دستگاه بدون رابطه'); $service = $this->service($section, 'سرویس بدون رابطه'); // هیچ ردیفی برای این سرویس نیست → همان سازگاری عقب‌روِ `findEligible`. [$res] = $this->book([ 'doctor_uuid' => $doctor->getUuid(), 'clinic_uuid' => $clinic->getUuid(), 'resource_uuid' => $resource->getUuid(), 'service_item_uuids' => [$service->getUuid()], ]); self::assertSame(201, $this->responseCode(), json_encode($res, JSON_UNESCAPED_UNICODE)); } public function testAnInactiveResourceCannotBeBooked(): void { [$clinic, $doctor, $address] = $this->clinic(); $resource = $this->resource($clinic, $address, 'دستگاه خاموش', $doctor); $resource->setActive(false); $this->em->flush(); $this->book(['resource_uuid' => $resource->getUuid()]); self::assertSame(422, $this->responseCode()); } }