Pick candidate resources by whether they actually offer the service
findEligible matched on address, type and skills, so two devices of the same type were interchangeable even when only one of them performed the service. It now also consults the offering table. The filter is conditional on purpose: it only applies once the clinic has registered at least one resource for that service. Applying it unconditionally would leave every environment that has not filled the links in yet without a single free slot overnight — a silent outage caused by a feature they never opted into. When rows do exist but all are inactive the result is empty, which is the honest answer: nobody performs this right now. The service comes from the segment template rather than the root service. One appointment's plan can carry segments from several items, and "who can do this" is a per-item question. Five tests: the filter picking one of two identical devices, the no-rows passthrough, the all-inactive empty, the no-service-argument path still untouched, and the filter stacking with the skill filter. Suite 1277 green — including the 27 existing plan and availability tests, which is what proves the backward-compatible path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Resource;
|
||||
|
||||
use App\ClinicService\Entity\ServiceItem;
|
||||
use App\ClinicService\Entity\ServiceSection;
|
||||
use App\Doctor\Entity\DoctorAddress;
|
||||
use App\Resource\Entity\ClinicResource;
|
||||
use App\Resource\Entity\ResourceServiceOffering;
|
||||
use App\Resource\Entity\ResourceType;
|
||||
use App\Resource\Repository\ClinicResourceRepository;
|
||||
|
||||
/**
|
||||
* انتخاب کاندید بر اساس «کدام منبع این سرویس را میدهد».
|
||||
*
|
||||
* پیش از این، دو دستگاه همنوع غیرقابلتفکیک بودند حتی وقتی فقط یکیشان آن سرویس را
|
||||
* انجام میداد. فیلتر عمداً **مشروط** است: محیطی که هنوز رابطهها را پر نکرده نباید
|
||||
* یکشبه بیوقت شود.
|
||||
*/
|
||||
class OfferingEligibilityTest extends ResourceTestCase
|
||||
{
|
||||
private function service(DoctorAddress $address, string $name): ServiceItem
|
||||
{
|
||||
$section = new ServiceSection($address->tenantEntityType(), $address->tenantEntityId(), 'بخش ' . $name);
|
||||
$this->em->persist($section);
|
||||
$this->em->flush();
|
||||
|
||||
$item = new ServiceItem($section, $name, 5_000_000);
|
||||
$item->setDurationMinutes(30);
|
||||
$this->em->persist($item);
|
||||
$this->em->flush();
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function resource(DoctorAddress $address, ResourceType $type, string $name): ClinicResource
|
||||
{
|
||||
$resource = new ClinicResource($address, $type, $name);
|
||||
$this->em->persist($resource);
|
||||
$this->em->flush();
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
private function repo(): ClinicResourceRepository
|
||||
{
|
||||
return $this->em->getRepository(ClinicResource::class);
|
||||
}
|
||||
|
||||
// ── ✅ موفق ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testOnlyTheResourceThatOffersTheServiceIsEligible(): void
|
||||
{
|
||||
[, , $address] = $this->clinicWithAddress();
|
||||
$type = $this->resourceType($address);
|
||||
$service = $this->service($address, 'لیزر CO2');
|
||||
|
||||
$offering = $this->resource($address, $type, 'دستگاه شمارهٔ ۱');
|
||||
$this->resource($address, $type, 'دستگاه شمارهٔ ۲'); // این یکی وصل نیست
|
||||
|
||||
$this->em->persist(new ResourceServiceOffering($offering, $service));
|
||||
$this->em->flush();
|
||||
|
||||
$eligible = $this->repo()->findEligible($address, $type, [], $service);
|
||||
|
||||
self::assertCount(1, $eligible);
|
||||
self::assertSame('دستگاه شمارهٔ ۱', $eligible[0]->getName());
|
||||
}
|
||||
|
||||
// ── ⚠️ مرزی: سازگاری عقبرو ──────────────────────────────────────────────
|
||||
|
||||
public function testWithoutAnyOfferingTheFilterIsNotAppliedAtAll(): void
|
||||
{
|
||||
[, , $address] = $this->clinicWithAddress();
|
||||
$type = $this->resourceType($address);
|
||||
$service = $this->service($address, 'سرویس بدون رابطه');
|
||||
|
||||
$this->resource($address, $type, 'دستگاه الف');
|
||||
$this->resource($address, $type, 'دستگاه ب');
|
||||
|
||||
// هیچ ردیفی برای این سرویس نیست → همان رفتار قبلی، هر دو کاندیدند.
|
||||
self::assertCount(2, $this->repo()->findEligible($address, $type, [], $service));
|
||||
}
|
||||
|
||||
public function testDeactivatingEveryOfferingLeavesNoCandidate(): void
|
||||
{
|
||||
[, , $address] = $this->clinicWithAddress();
|
||||
$type = $this->resourceType($address);
|
||||
$service = $this->service($address, 'لیزر خاموش');
|
||||
|
||||
$first = $this->resource($address, $type, 'دستگاه الف');
|
||||
$this->resource($address, $type, 'دستگاه ب');
|
||||
|
||||
$offering = new ResourceServiceOffering($first, $service);
|
||||
$offering->setActive(false);
|
||||
$this->em->persist($offering);
|
||||
$this->em->flush();
|
||||
|
||||
// ردیف هست ولی خاموش: یعنی «کسی این را نمیدهد»، نه «فیلتری در کار نیست».
|
||||
self::assertSame([], $this->repo()->findEligible($address, $type, [], $service));
|
||||
}
|
||||
|
||||
public function testWithoutAServiceTheOldBehaviourIsUntouched(): void
|
||||
{
|
||||
[, , $address] = $this->clinicWithAddress();
|
||||
$type = $this->resourceType($address);
|
||||
$service = $this->service($address, 'لیزر');
|
||||
|
||||
$only = $this->resource($address, $type, 'دستگاه تنها');
|
||||
$this->resource($address, $type, 'دستگاه دیگر');
|
||||
$this->em->persist(new ResourceServiceOffering($only, $service));
|
||||
$this->em->flush();
|
||||
|
||||
// بدون سرویس، هیچ فیلتری اعمال نمیشود — مسیرهایی که سرویس ندارند نباید بشکنند.
|
||||
self::assertCount(2, $this->repo()->findEligible($address, $type));
|
||||
}
|
||||
|
||||
public function testTheFilterStacksWithTheSkillFilter(): void
|
||||
{
|
||||
[, , $address] = $this->clinicWithAddress();
|
||||
$type = $this->resourceType($address);
|
||||
$service = $this->service($address, 'لیزر تخصصی');
|
||||
|
||||
$skilled = $this->resource($address, $type, 'دستگاه با مهارت');
|
||||
$plain = $this->resource($address, $type, 'دستگاه بیمهارت');
|
||||
|
||||
$skill = new \App\Resource\Entity\Skill($address->tenantEntityType(), $address->tenantEntityId(), 'کار با لیزر');
|
||||
$this->em->persist($skill);
|
||||
$this->em->flush();
|
||||
$this->em->persist(new \App\Resource\Entity\ResourceSkill($skilled, $skill, 3));
|
||||
|
||||
// هر دو سرویس را میدهند، ولی فقط یکی مهارتش را دارد.
|
||||
$this->em->persist(new ResourceServiceOffering($skilled, $service));
|
||||
$this->em->persist(new ResourceServiceOffering($plain, $service));
|
||||
$this->em->flush();
|
||||
|
||||
$eligible = $this->repo()->findEligible($address, $type, [(int) $skill->getId()], $service);
|
||||
|
||||
self::assertCount(1, $eligible);
|
||||
self::assertSame('دستگاه با مهارت', $eligible[0]->getName());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user