resolver = new ResourceServiceResolver( $this->em->getRepository(ResourceServiceOffering::class), $this->em->getRepository(ServiceBranchOverride::class), ); $user = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']); $clinic = new Clinic($user); $clinic->setName('کلینیک زنجیره'); $this->em->persist($clinic); $this->em->flush(); $this->address = DoctorAddress::forClinic($clinic->getId()); $this->address->setName('شعبهٔ مرکزی'); $this->em->persist($this->address); $type = new ResourceType('clinic', (int) $clinic->getId(), 'device', 'دستگاه'); $this->em->persist($type); $this->em->flush(); $this->resource = new ClinicResource($this->address, $type, 'لیزر دایود'); $this->em->persist($this->resource); $section = new ServiceSection('clinic', (int) $clinic->getId(), 'لیزر'); $this->em->persist($section); $this->em->flush(); // سرویسِ والد «لیزر» و گزینهٔ «پا» — هر دو ServiceItem اند، همان مدل کاتالوگ. $this->service = new ServiceItem($section, 'لیزر', 10_000_000); $this->service->setDurationMinutes(60); $this->em->persist($this->service); $this->option = new ServiceItem($section, 'لیزر پا', 8_000_000); $this->option->setDurationMinutes(30); $this->em->persist($this->option); $this->em->flush(); } private function offer(ServiceItem $item, ?int $minutes, ?int $price, bool $active = true): ResourceServiceOffering { $offering = new ResourceServiceOffering($this->resource, $item); $offering->setDurationMinutes($minutes)->setPriceRials($price)->setActive($active); $this->em->persist($offering); $this->em->flush(); return $offering; } private function branchOverride(ServiceItem $item, ?int $minutes): void { $override = new ServiceBranchOverride($item, $this->address); $override->setSoloDurationMinutes($minutes); $this->em->persist($override); $this->em->flush(); } private function resolve(): ResolvedServiceSpec { return $this->resolver->resolve($this->resource, $this->option, $this->address, $this->service); } // ── ✅ هر سطح، تنها ────────────────────────────────────────────────────── public function testLevelOneResourcePlusOptionWins(): void { $this->offer($this->option, 15, 9_500_000); $this->offer($this->service, 40, 12_000_000); $this->branchOverride($this->option, 50); $spec = $this->resolve(); self::assertSame(15, $spec->durationMinutes); self::assertSame(9_500_000, $spec->priceRials); self::assertSame(ResolvedServiceSpec::SOURCE_RESOURCE_OPTION, $spec->durationSource); self::assertSame(ResolvedServiceSpec::SOURCE_RESOURCE_OPTION, $spec->priceSource); } public function testLevelTwoResourcePlusServiceWinsWhenTheOptionHasNothing(): void { $this->offer($this->service, 40, 12_000_000); $this->branchOverride($this->option, 50); $spec = $this->resolve(); self::assertSame(40, $spec->durationMinutes); self::assertSame(12_000_000, $spec->priceRials); self::assertSame(ResolvedServiceSpec::SOURCE_RESOURCE_SERVICE, $spec->durationSource); } /** شعبه فقط مدت می‌دهد؛ قیمتش را از خودِ سرویس می‌گیرد. */ public function testLevelThreeBranchWinsForDurationOnly(): void { $this->branchOverride($this->option, 50); $spec = $this->resolve(); self::assertSame(50, $spec->durationMinutes); self::assertSame(ResolvedServiceSpec::SOURCE_BRANCH, $spec->durationSource); self::assertSame(8_000_000, $spec->priceRials); self::assertSame(ResolvedServiceSpec::SOURCE_SERVICE_DEFAULT, $spec->priceSource); } public function testLevelFourFallsBackToTheItemItself(): void { $spec = $this->resolve(); self::assertSame(30, $spec->durationMinutes); self::assertSame(8_000_000, $spec->priceRials); self::assertSame(ResolvedServiceSpec::SOURCE_SERVICE_DEFAULT, $spec->durationSource); self::assertSame(ResolvedServiceSpec::SOURCE_SERVICE_DEFAULT, $spec->priceSource); } // ── ⚠️ مرزی ────────────────────────────────────────────────────────────── public function testDurationAndPriceResolveIndependently(): void { // منبع فقط مدت را می‌گوید؛ قیمت باید تا خودِ سرویس پایین برود. $this->offer($this->option, 15, null); $this->branchOverride($this->option, null); $spec = $this->resolve(); self::assertSame(15, $spec->durationMinutes); self::assertSame(ResolvedServiceSpec::SOURCE_RESOURCE_OPTION, $spec->durationSource); self::assertSame(8_000_000, $spec->priceRials); self::assertSame(ResolvedServiceSpec::SOURCE_SERVICE_DEFAULT, $spec->priceSource); } public function testAnInactiveOfferingIsSkippedEntirely(): void { // ردیف هست ولی خاموش: «این منبع فعلاً این را نمی‌دهد» یعنی اعدادش هم خوانده نمی‌شوند. $this->offer($this->option, 15, 9_500_000, active: false); $spec = $this->resolve(); self::assertSame(30, $spec->durationMinutes); self::assertSame(8_000_000, $spec->priceRials); self::assertSame(ResolvedServiceSpec::SOURCE_SERVICE_DEFAULT, $spec->durationSource); } public function testAFreeServiceKeepsItsZeroInsteadOfInheriting(): void { // صفر مقدار واقعی است، نه «حرفی ندارم» — وگرنه سرویس رایگان قیمت سرویس والد را می‌گرفت. $this->offer($this->option, null, 0); $spec = $this->resolve(); self::assertSame(0, $spec->priceRials); self::assertSame(ResolvedServiceSpec::SOURCE_RESOURCE_OPTION, $spec->priceSource); } public function testResolvingTheServiceItselfNeedsNoParent(): void { $this->offer($this->service, 45, 13_000_000); $spec = $this->resolver->resolve($this->resource, $this->service, $this->address); self::assertSame(45, $spec->durationMinutes); self::assertSame(13_000_000, $spec->priceRials); self::assertSame(ResolvedServiceSpec::SOURCE_RESOURCE_OPTION, $spec->durationSource); } }