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>
185 lines
8.6 KiB
PHP
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']);
|
|
}
|
|
}
|