createUser(['ROLE_USER', 'ROLE_CLINIC']); $clinic = new Clinic($user); $clinic->setName('کلینیک تایم‌لاین'); $this->em->persist($clinic); $this->em->flush(); $address = DoctorAddress::forClinic($clinic->getId()); $address->setName('شعبهٔ مرکزی'); $this->em->persist($address); $this->em->flush(); $type = new ResourceType('clinic', (int) $clinic->getId(), 'device', 'دستگاه لیزر'); $this->em->persist($type); $this->em->flush(); $resource = new ClinicResource($address, $type, 'لیزر دایود'); $this->em->persist($resource); $this->em->flush(); return [$user, $resource]; } private function occupy(ClinicResource $resource, int $start, int $end, string $segment): void { $occupancy = new ResourceOccupancy($resource, $start, $end); $occupancy->setSegmentName($segment); $this->em->persist($occupancy); $this->em->flush(); } private function shift(ClinicResource $resource, int $day, int $from, int $to): void { $this->em->persist(new ResourceCalendar($resource, $day, $from, $to)); $this->em->flush(); } /** ۰ = شنبه، همان قرارداد تقویم منبع. */ private function dayOfWeek(int $timestamp): int { return (int) (((int) date('w', $timestamp) + 1) % 7); } // ── ✅ موفق ────────────────────────────────────────────────────────────── public function testTheDayShowsEachResourceWithItsOccupiedRanges(): void { [$user, $resource] = $this->clinicWithResource(); $midnight = (int) strtotime('today midnight'); $this->shift($resource, $this->dayOfWeek($midnight), 540, 1020); $this->occupy($resource, $midnight + 10 * 3600, $midnight + 11 * 3600, 'لیزر'); $body = $this->authJson('GET', '/api/v1/resources/timeline?date=' . date('Y-m-d', $midnight), $user); self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE)); self::assertSame($midnight, $body['data']['date']); $row = $body['data']['resources'][0]; self::assertSame('لیزر دایود', $row['name']); self::assertSame('دستگاه لیزر', $row['type_name']); self::assertSame([['start_minute' => 540, 'end_minute' => 1020]], $row['shifts']); self::assertCount(1, $row['items']); self::assertSame('لیزر', $row['items'][0]['segment_name']); self::assertSame($midnight + 10 * 3600, $row['items'][0]['starts_at']); } public function testOnlyTheShiftsOfThatWeekdayComeBack(): void { [$user, $resource] = $this->clinicWithResource(); $midnight = (int) strtotime('today midnight'); $today = $this->dayOfWeek($midnight); $this->shift($resource, $today, 540, 1020); // شیفت روز دیگر نباید در تایم‌لاین امروز ظاهر شود. $this->shift($resource, ($today + 3) % 7, 60, 120); $body = $this->authJson('GET', '/api/v1/resources/timeline?date=' . date('Y-m-d', $midnight), $user); self::assertSame([['start_minute' => 540, 'end_minute' => 1020]], $body['data']['resources'][0]['shifts']); } // ── ⚠️ مرزی ───────────────────────────────────────────────────────────── public function testARestDayReturnsTheResourceWithNothingOnIt(): void { [$user, $resource] = $this->clinicWithResource(); $midnight = (int) strtotime('today midnight'); $this->occupy($resource, $midnight - 5 * 86400, $midnight - 5 * 86400 + 3600, 'هفتهٔ پیش'); $body = $this->authJson('GET', '/api/v1/resources/timeline?date=' . date('Y-m-d', $midnight), $user); // منبع می‌ماند تا ردیفش در تایم‌لاین دیده شود؛ فقط خالی است. self::assertCount(1, $body['data']['resources']); self::assertSame([], $body['data']['resources'][0]['items']); self::assertSame([], $body['data']['resources'][0]['shifts']); } public function testWithoutADateItFallsBackToToday(): void { [$user] = $this->clinicWithResource(); $body = $this->authJson('GET', '/api/v1/resources/timeline', $user); self::assertSame(200, $this->responseCode()); self::assertSame((int) strtotime('today midnight'), $body['data']['date']); } // ── ❌ خطا ─────────────────────────────────────────────────────────────── public function testAMalformedDateIsRefused(): void { [$user] = $this->clinicWithResource(); $this->authJson('GET', '/api/v1/resources/timeline?date=05-08-2026', $user); self::assertSame(422, $this->responseCode()); } public function testAnotherEnvironmentDoesNotSeeTheseResources(): void { [, $resource] = $this->clinicWithResource(); $midnight = (int) strtotime('today midnight'); $this->occupy($resource, $midnight + 3600, $midnight + 7200, 'لیزر'); [$stranger] = $this->clinicWithResource(); $body = $this->authJson('GET', '/api/v1/resources/timeline?date=' . date('Y-m-d', $midnight), $stranger); // منبعِ خودش را می‌بیند، نه منبع کلینیک دیگر. self::assertCount(1, $body['data']['resources']); self::assertSame([], $body['data']['resources'][0]['items']); } // ── وقت آزاد و فیلتر «قابل رزرو» ──────────────────────────────────────── /** ردیف تایم‌لاین باید بگوید چقدر از شیفت هنوز جا دارد، نه فقط چه چیزی گرفته است. */ public function testTheLaneReportsHowMuchOfTheShiftIsStillFree(): void { [$user, $resource] = $this->clinicWithResource(); $midnight = (int) strtotime('today midnight'); $this->shift($resource, $this->dayOfWeek($midnight), 540, 1020); // ۸ ساعت $this->occupy($resource, $midnight + 10 * 3600, $midnight + 11 * 3600, 'لیزر'); $row = $this->authJson('GET', '/api/v1/resources/timeline?date=' . date('Y-m-d', $midnight), $user) ['data']['resources'][0]; self::assertSame(480, $row['shift_minutes']); self::assertSame(60, $row['busy_minutes']); self::assertSame(420, $row['free_minutes']); } /** منبعی که به هیچ سرویسی وصل نیست، ردیفِ همیشه‌خالی در تایم‌لاین نمی‌سازد. */ public function testOnlyBookableHidesResourcesWithNoService(): void { [$user, $resource] = $this->clinicWithResource(); $midnight = (int) strtotime('today midnight'); $this->shift($resource, $this->dayOfWeek($midnight), 540, 1020); $all = $this->authJson('GET', '/api/v1/resources/timeline?date=' . date('Y-m-d', $midnight), $user); self::assertCount(1, $all['data']['resources']); $bookable = $this->authJson( 'GET', '/api/v1/resources/timeline?only_bookable=1&date=' . date('Y-m-d', $midnight), $user, ); self::assertSame([], $bookable['data']['resources'], 'بدون ResourceServiceOffering فعال، قابل رزرو نیست'); } }