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); $section = new ServiceSection('clinic', (int) $clinic->getId(), 'لیزر'); $this->em->persist($section); $service = new ServiceItem($section, 'لیزر فول بادی', 10_000_000); $this->em->persist($service); $this->em->flush(); return [$user, $clinic, $service, $address]; } private function staffFor(Clinic $clinic, string $name): ClinicStaff { $staff = new ClinicStaff('clinic', (int) $clinic->getId(), $name); $this->em->persist($staff); $this->em->flush(); return $staff; } private function doctorIn(Clinic $clinic): Doctor { $user = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']); $doctor = new Doctor($user, 'دکتر ناظر'); $doctor->setMobileNumber($user->getMobileNumber()); $this->em->persist($doctor); $this->em->flush(); $clinic->getDoctors()->add($doctor); $this->em->flush(); return $doctor; } /** سناریوی بوتاکس: جلسه ۲ بعد از ۱۵ روز، بقیه ماهانه — فاصلهٔ ثابت این را نمی‌گیرد. */ public function testProtocolWithUnevenIntervalsIsStoredAndReadBack(): void { [$user, $clinic, $service] = $this->clinicWithService(); $staff = $this->staffFor($clinic, 'اپراتور یک'); $supervisor = $this->doctorIn($clinic); $uri = '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol'; $body = $this->authJson('PUT', $uri, $user, [ 'supervisor_doctor_uuid' => $supervisor->getUuid(), 'staff_uuids' => [$staff->getUuid()], 'steps' => [ ['step_number' => 1, 'offset_days' => 0], ['step_number' => 2, 'offset_days' => 15], ['step_number' => 3, 'offset_days' => 30], ['step_number' => 4, 'offset_days' => 30], ], ]); self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE)); self::assertSame(4, $body['data']['total_sessions']); self::assertSame([0, 15, 30, 30], array_column($body['data']['steps'], 'offset_days')); self::assertSame($supervisor->getUuid(), $body['data']['supervisor']['uuid']); self::assertSame([$staff->getUuid()], array_column($body['data']['staff'], 'uuid')); $read = $this->authJson('GET', $uri, $user); self::assertSame([0, 15, 30, 30], array_column($read['data']['steps'], 'offset_days')); } /** سرویس بدون پروتکل یعنی سوییچ خاموش، نه «پیدا نشد». */ public function testServiceWithoutProtocolReturnsNull(): void { [$user, , $service] = $this->clinicWithService(); $body = $this->authJson('GET', '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol', $user); self::assertSame(200, $this->responseCode()); self::assertNull($body['data']); } public function testReplacingAProtocolDropsTheOldSteps(): void { [$user, $clinic, $service] = $this->clinicWithService(); $staff = $this->staffFor($clinic, 'اپراتور یک'); $uri = '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol'; $this->authJson('PUT', $uri, $user, [ 'staff_uuids' => [$staff->getUuid()], 'steps' => [ ['step_number' => 1, 'offset_days' => 0], ['step_number' => 2, 'offset_days' => 30], ['step_number' => 3, 'offset_days' => 30], ], ]); $body = $this->authJson('PUT', $uri, $user, [ 'staff_uuids' => [$staff->getUuid()], 'steps' => [ ['step_number' => 1, 'offset_days' => 0], ['step_number' => 2, 'offset_days' => 21], ], ]); self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE)); self::assertSame(2, $body['data']['total_sessions']); self::assertSame([0, 21], array_column($body['data']['steps'], 'offset_days')); } public function testDeleteTurnsTheSwitchOff(): void { [$user, $clinic, $service] = $this->clinicWithService(); $staff = $this->staffFor($clinic, 'اپراتور یک'); $uri = '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol'; $this->authJson('PUT', $uri, $user, [ 'staff_uuids' => [$staff->getUuid()], 'steps' => [ ['step_number' => 1, 'offset_days' => 0], ['step_number' => 2, 'offset_days' => 30], ], ]); $this->authJson('DELETE', $uri, $user); self::assertSame(200, $this->responseCode()); $body = $this->authJson('GET', $uri, $user); self::assertNull($body['data']); } /** یک گام یعنی سرویس تک‌جلسه‌ای — یعنی سوییچ اصلاً نباید روشن می‌شد. */ public function testSingleStepProtocolIsRejected(): void { [$user, $clinic, $service] = $this->clinicWithService(); $staff = $this->staffFor($clinic, 'اپراتور یک'); $body = $this->authJson('PUT', '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol', $user, [ 'staff_uuids' => [$staff->getUuid()], 'steps' => [['step_number' => 1, 'offset_days' => 0]], ]); self::assertSame(422, $this->responseCode()); self::assertSame('steps', $body['errors'][0]['field']); } public function testFirstStepMustHaveZeroOffset(): void { [$user, $clinic, $service] = $this->clinicWithService(); $staff = $this->staffFor($clinic, 'اپراتور یک'); $body = $this->authJson('PUT', '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol', $user, [ 'staff_uuids' => [$staff->getUuid()], 'steps' => [ ['step_number' => 1, 'offset_days' => 10], ['step_number' => 2, 'offset_days' => 30], ], ]); self::assertSame(422, $this->responseCode()); self::assertSame('offset_days', $body['errors'][0]['field']); } public function testLaterStepsNeedAPositiveOffset(): void { [$user, $clinic, $service] = $this->clinicWithService(); $staff = $this->staffFor($clinic, 'اپراتور یک'); $body = $this->authJson('PUT', '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol', $user, [ 'staff_uuids' => [$staff->getUuid()], 'steps' => [ ['step_number' => 1, 'offset_days' => 0], ['step_number' => 2, 'offset_days' => 0], ], ]); self::assertSame(422, $this->responseCode()); self::assertSame('offset_days', $body['errors'][0]['field']); } /** «جلسهٔ ۳ از ۸» فقط وقتی معنی دارد که گامی جا نیفتاده باشد. */ public function testStepNumbersMustBeContiguous(): void { [$user, $clinic, $service] = $this->clinicWithService(); $staff = $this->staffFor($clinic, 'اپراتور یک'); $body = $this->authJson('PUT', '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol', $user, [ 'staff_uuids' => [$staff->getUuid()], 'steps' => [ ['step_number' => 1, 'offset_days' => 0], ['step_number' => 3, 'offset_days' => 30], ], ]); self::assertSame(422, $this->responseCode()); self::assertSame('step_number', $body['errors'][0]['field']); } public function testStaffIsRequired(): void { [$user, , $service] = $this->clinicWithService(); $body = $this->authJson('PUT', '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol', $user, [ 'staff_uuids' => [], 'steps' => [ ['step_number' => 1, 'offset_days' => 0], ['step_number' => 2, 'offset_days' => 30], ], ]); self::assertSame(422, $this->responseCode()); self::assertSame('staff_uuids', $body['errors'][0]['field']); } /** پرسنل با uuid از خودِ درخواست می‌آید و TenantFilter پوششش نمی‌دهد. */ public function testStaffFromAnotherEnvironmentIsRejected(): void { [$user, , $service] = $this->clinicWithService(); [, $otherClinic] = $this->clinicWithService(); $foreignStaff = $this->staffFor($otherClinic, 'اپراتور بیگانه'); $this->authJson('PUT', '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol', $user, [ 'staff_uuids' => [$foreignStaff->getUuid()], 'steps' => [ ['step_number' => 1, 'offset_days' => 0], ['step_number' => 2, 'offset_days' => 30], ], ]); self::assertSame(404, $this->responseCode()); } public function testServiceFromAnotherEnvironmentIsNotFound(): void { [$user] = $this->clinicWithService(); [, , $otherService] = $this->clinicWithService(); $this->authJson('GET', '/api/v1/service-item/' . $otherService->getUuid() . '/treatment-protocol', $user); self::assertSame(404, $this->responseCode()); } public function testMaxStepsIsEnforced(): void { [$user, $clinic, $service] = $this->clinicWithService(); $staff = $this->staffFor($clinic, 'اپراتور یک'); $steps = [['step_number' => 1, 'offset_days' => 0]]; for ($i = 2; $i <= TreatmentProtocol::MAX_STEPS + 1; $i++) { $steps[] = ['step_number' => $i, 'offset_days' => 30]; } $body = $this->authJson('PUT', '/api/v1/service-item/' . $service->getUuid() . '/treatment-protocol', $user, [ 'staff_uuids' => [$staff->getUuid()], 'steps' => $steps, ]); self::assertSame(422, $this->responseCode()); self::assertSame('steps', $body['errors'][0]['field']); } }