Files
clinicpro/tests/Resource/ResourceServiceOfferingTest.php
T
hamedandClaude Opus 5 1543eddd9e feat(resource): expose each offering's service section
The resource booking modal groups services under their section, the way the
doctor's service booking does. The offering list had no section, so the list
could only be flat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 13:47:39 +03:30

185 lines
8.6 KiB
PHP

<?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 Doctrine\DBAL\Exception\UniqueConstraintViolationException;
/**
* «کدام منبع کدام سرویس را می‌دهد، با چه مدت و چه قیمتی.»
*
* تا پیش از این، انتخاب منبع فقط از نوع و مهارت می‌آمد؛ یعنی دو دستگاه هم‌نوع
* غیرقابل‌تفکیک بودند حتی وقتی فقط یکی‌شان آن سرویس را می‌داد.
*/
class ResourceServiceOfferingTest extends ResourceTestCase
{
private function service(DoctorAddress $address, string $name, int $price = 5_000_000): ServiceItem
{
$section = new ServiceSection($address->tenantEntityType(), $address->tenantEntityId(), 'بخش ' . $name);
$this->em->persist($section);
$this->em->flush();
$item = new ServiceItem($section, $name, $price);
$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;
}
// ── ✅ موفق ──────────────────────────────────────────────────────────────
public function testAnOfferingCarriesTheDurationAndPriceOfThatResource(): void
{
[, , $address] = $this->clinicWithAddress();
$type = $this->resourceType($address);
$resource = $this->resource($address, $type, 'لیزر دایود ۲');
$service = $this->service($address, 'لیزر پا', 8_000_000);
$offering = new ResourceServiceOffering($resource, $service);
$offering->setDurationMinutes(15)->setPriceRials(9_500_000);
$this->em->persist($offering);
$this->em->flush();
$this->em->clear();
$stored = $this->em->getRepository(ResourceServiceOffering::class)->find($offering->getId());
self::assertSame(15, $stored->getDurationMinutes());
self::assertSame(9_500_000, (int) $stored->getPriceRials());
self::assertTrue($stored->isActive());
// قیمت خودِ سرویس دست‌نخورده می‌ماند؛ این ردیف رویش می‌نشیند، جایش را نمی‌گیرد.
self::assertSame(8_000_000, $stored->getServiceItem()->getPriceRials());
}
public function testNullMeansInheritNotZero(): void
{
[, , $address] = $this->clinicWithAddress();
$type = $this->resourceType($address);
$resource = $this->resource($address, $type, 'لیزر بدون تنظیم');
$service = $this->service($address, 'لیزر دست');
$offering = new ResourceServiceOffering($resource, $service);
$this->em->persist($offering);
$this->em->flush();
self::assertNull($offering->getDurationMinutes());
self::assertNull($offering->getPriceRials());
// صفر «مدتی که وجود ندارد» است، نه «حرفی برای گفتن ندارم» — پس رد می‌شود.
$this->expectException(\InvalidArgumentException::class);
$offering->setDurationMinutes(0);
}
// ── ❌ خطا ───────────────────────────────────────────────────────────────
public function testTheSamePairCannotBeRegisteredTwice(): void
{
[, , $address] = $this->clinicWithAddress();
$type = $this->resourceType($address);
$resource = $this->resource($address, $type, 'لیزر تکراری');
$service = $this->service($address, 'لیزر صورت');
$this->em->persist(new ResourceServiceOffering($resource, $service));
$this->em->flush();
$this->em->persist(new ResourceServiceOffering($resource, $service));
$this->expectException(UniqueConstraintViolationException::class);
$this->em->flush();
}
public function testAResourceCannotOfferAServiceFromAnotherEnvironment(): void
{
[, , $address] = $this->clinicWithAddress();
[, , $otherAddress] = $this->clinicWithAddress('شعبهٔ کلینیک دیگر');
$resource = $this->resource($address, $this->resourceType($address), 'لیزر محیط الف');
$foreign = $this->service($otherAddress, 'سرویس محیط ب');
// `TenantFilter` این را نمی‌گیرد: هر دو uuid از بدنهٔ درخواست می‌آیند و فیلتر
// روی فرزند aggregate اعمال نمی‌شود. پس گارد باید در خودِ سازنده باشد.
$this->expectException(\InvalidArgumentException::class);
new ResourceServiceOffering($resource, $foreign);
}
// ── ⚠️ مرزی ──────────────────────────────────────────────────────────────
public function testAServiceCanBeOfferedByMoreThanOneResourceWithDifferentNumbers(): void
{
[, , $address] = $this->clinicWithAddress();
$type = $this->resourceType($address);
$service = $this->service($address, 'لیزر پا');
$first = $this->resource($address, $type, 'دستگاه شمارهٔ ۱');
$second = $this->resource($address, $type, 'دستگاه شمارهٔ ۲');
$this->em->persist((new ResourceServiceOffering($first, $service))->setDurationMinutes(20)->setPriceRials(8_000_000));
$this->em->persist((new ResourceServiceOffering($second, $service))->setDurationMinutes(15)->setPriceRials(9_500_000));
$this->em->flush();
$repo = $this->em->getRepository(ResourceServiceOffering::class);
self::assertTrue($repo->hasAnyFor($service));
self::assertCount(2, $repo->activeResourceIdsFor($service));
// همان مثال سند: یک سرویس، دو دستگاه، دو عدد متفاوت.
self::assertSame(20, $repo->findOneFor($first, $service)?->getDurationMinutes());
self::assertSame(15, $repo->findOneFor($second, $service)?->getDurationMinutes());
}
public function testDeactivatingKeepsTheRowAndItsNumbers(): void
{
[, , $address] = $this->clinicWithAddress();
$type = $this->resourceType($address);
$resource = $this->resource($address, $type, 'دستگاه خاموش');
$service = $this->service($address, 'لیزر بیکینی');
$offering = new ResourceServiceOffering($resource, $service);
$offering->setDurationMinutes(25)->setPriceRials(7_000_000)->setActive(false);
$this->em->persist($offering);
$this->em->flush();
$repo = $this->em->getRepository(ResourceServiceOffering::class);
// رابطه هست ولی فعال نیست: تنظیمات با خاموش/روشن کردن از دست نمی‌رود.
self::assertTrue($repo->hasAnyFor($service));
self::assertSame([], $repo->activeResourceIdsFor($service));
self::assertSame(25, $repo->findOneFor($resource, $service)?->getDurationMinutes());
}
/** مودال رزرو منبع سرویس‌ها را زیر بخششان گروه می‌کند، پس فهرست باید بخش را بدهد. */
public function testOfferingListCarriesTheServiceSection(): void
{
[$user, , $address] = $this->clinicWithAddress();
$resource = $this->resource($address, $this->resourceType($address), 'لیزر الکساندرایت');
$service = $this->service($address, 'لیزر زیربغل');
$this->authJson('PUT', "/api/v1/resource/{$resource->getUuid()}/services", $user, [
'services' => [['service_uuid' => $service->getUuid()]],
]);
self::assertSame(200, $this->responseCode());
$body = $this->authJson('GET', "/api/v1/resource/{$resource->getUuid()}/services", $user);
self::assertSame(200, $this->responseCode());
self::assertSame(
$service->getSection()->getUuid(),
$body['data'][0]['service_section']['uuid'],
);
self::assertSame('بخش لیزر زیربغل', $body['data'][0]['service_section']['name']);
}
}