createUser(['ROLE_USER', 'ROLE_CLINIC']); $clinic = new Clinic($user); $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); $type = new ResourceType('clinic', (int) $clinic->getId(), 'laser_' . bin2hex(random_bytes(3)), 'لیزر'); $this->em->persist($type); $this->em->flush(); $resource = new ClinicResource($address, $type, 'دستگاه لیزر'); $resource->setSupervisor($doctor); $this->em->persist($resource); $section = new ServiceSection('clinic', (int) $clinic->getId(), 'لیزر'); $this->em->persist($section); $category = new CatalogCategory('clinic', (int) $clinic->getId(), 'دست'); $this->em->persist($category); $service = new ServiceItem($section, 'لیزر دست', 5_000_000); $service->setCatalogCategory($category)->setDurationMinutes(30); $this->em->persist($service); $staff = new ClinicStaff('clinic', (int) $clinic->getId(), 'اپراتور'); $this->em->persist($staff); $protocol = new TreatmentProtocol($service); $this->em->persist($protocol); $protocol->replaceSteps([ new TreatmentProtocolStep($protocol, 1, 0), new TreatmentProtocolStep($protocol, 2, 30), ]); $protocol->replaceAllowedStaff([new TreatmentProtocolStaff($protocol, $staff)]); $record = new PatientRecord('clinic', (int) $clinic->getId(), $this->createUser(), 'clinic', (int) $clinic->getId()); $this->em->persist($record); $this->em->flush(); $case = new TreatmentCase('clinic', (int) $clinic->getId(), $record, $service, $protocol); $case->addArea(new TreatmentCaseArea($case, $category, 0)); $first = new TreatmentSession($case, 1); $second = new TreatmentSession($case, 2); $case->addSession($first); $case->addSession($second); $this->em->persist($case); $this->em->flush(); return ['user' => $user, 'clinic' => $clinic, 'case' => $case, 'resource' => $resource, 'doctor' => $doctor]; } public function testListReturnsCasesOfTheCurrentTenantOnly(): void { $mine = $this->scenario(); $other = $this->scenario(); $body = $this->authJson('GET', '/api/v1/treatment-cases', $mine['user']); self::assertSame(200, $this->responseCode()); $uuids = array_column($body['data'], 'uuid'); self::assertContains($mine['case']->getUuid(), $uuids); self::assertNotContains($other['case']->getUuid(), $uuids); } public function testShowReturnsSessionsAndAreas(): void { $s = $this->scenario(); $body = $this->authJson('GET', '/api/v1/treatment-case/' . $s['case']->getUuid(), $s['user']); self::assertSame(200, $this->responseCode()); self::assertSame(2, $body['data']['total_sessions']); self::assertSame(0, $body['data']['completed_sessions']); self::assertCount(2, $body['data']['sessions']); self::assertSame(['دست'], array_column($body['data']['areas'], 'name')); } public function testACaseFromAnotherTenantIsNotFound(): void { $mine = $this->scenario(); $other = $this->scenario(); $this->authJson('GET', '/api/v1/treatment-case/' . $other['case']->getUuid(), $mine['user']); self::assertSame(404, $this->responseCode()); } /** جلسه‌ای که سررسیدش رسیده و رزرو نشده باید در صف دیده شود. */ public function testUnbookedQueueListsDueSessions(): void { $s = $this->scenario(); $sessions = $s['case']->getSessions()->toArray(); $sessions[0]->setDueAt(time() - 3600); $this->em->flush(); $body = $this->authJson('GET', '/api/v1/treatment-sessions/unbooked', $s['user']); self::assertSame(200, $this->responseCode()); $uuids = array_column($body['data'], 'uuid'); self::assertContains($sessions[0]->getUuid(), $uuids); // جلسهٔ دوم هنوز سررسید ندارد — لنگرش جلسهٔ اولِ انجام‌نشده است. self::assertNotContains($sessions[1]->getUuid(), $uuids); } public function testUnbookedQueueSkipsSessionsThatAlreadyHaveAnAppointment(): void { $s = $this->scenario(); $sessions = $s['case']->getSessions()->toArray(); $appointment = $this->newAppointment( $s['doctor'], $this->createUser(['ROLE_USER']), time() + 3600, time() + 5400, $s['clinic'], ); $this->em->persist($appointment); $this->em->flush(); $sessions[0]->setDueAt(time() - 3600); $sessions[0]->attachAppointment($appointment); $this->em->flush(); $body = $this->authJson('GET', '/api/v1/treatment-sessions/unbooked', $s['user']); self::assertNotContains($sessions[0]->getUuid(), array_column($body['data'], 'uuid')); } /** بدون منبع پیش‌فرض و بدون resource_uuid، باید صریح خطا بدهد نه فهرست خالی. */ public function testSlotSuggestionsWithoutAResourceIsRejected(): void { $s = $this->scenario(); $session = $s['case']->getSessions()->first(); $body = $this->authJson('GET', '/api/v1/treatment-session/' . $session->getUuid() . '/slot-suggestions', $s['user']); self::assertSame(422, $this->responseCode()); self::assertSame('resource_uuid', $body['errors'][0]['field']); } public function testSlotSuggestionsAcceptAnExplicitResource(): void { $s = $this->scenario(); $session = $s['case']->getSessions()->first(); $body = $this->authJson( 'GET', '/api/v1/treatment-session/' . $session->getUuid() . '/slot-suggestions?resource_uuid=' . $s['resource']->getUuid(), $s['user'], ); self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE)); self::assertSame($s['resource']->getUuid(), $body['data']['resource_uuid']); self::assertArrayHasKey('days', $body['data']); self::assertSame($session->getUuid(), $body['data']['session']['uuid']); } public function testAResourceFromAnotherTenantIsNotFound(): void { $mine = $this->scenario(); $other = $this->scenario(); $session = $mine['case']->getSessions()->first(); $this->authJson( 'GET', '/api/v1/treatment-session/' . $session->getUuid() . '/slot-suggestions?resource_uuid=' . $other['resource']->getUuid(), $mine['user'], ); self::assertSame(404, $this->responseCode()); } /** تقویم دوره: همهٔ جلسات با تاریخ، و جزئیات نواحیِ جلسهٔ انجام‌شده. */ public function testPlanReturnsEverySessionWithADate(): void { $s = $this->scenario(); $body = $this->authJson('GET', '/api/v1/treatment-case/' . $s['case']->getUuid() . '/plan', $s['user']); self::assertSame(200, $this->responseCode()); self::assertSame( $s['case']->getTotalSessions(), count($body['data']['sessions']), 'تقویم باید همهٔ جلسات را بدهد، نه فقط آن‌هایی که سررسید نوشته دارند', ); foreach ($body['data']['sessions'] as $row) { self::assertArrayHasKey('planned_at', $row); self::assertArrayHasKey('is_estimate', $row); // نواحی باید بیایند — «چه کاری انجام شد» همین است. self::assertArrayHasKey('areas', $row); } } /** حالت مرزی: پروندهٔ محیط دیگر — تقویم هم مثل بقیه ۴۰۴ می‌گیرد نه ۴۰۳. */ public function testPlanOfAnotherTenantIs404(): void { $mine = $this->scenario(); $other = $this->scenario(); $this->authJson('GET', '/api/v1/treatment-case/' . $other['case']->getUuid() . '/plan', $mine['user']); self::assertSame(404, $this->responseCode()); } }