Files
clinicpro/tests/ClinicService/ServiceItemPackageAndAuditTest.php
hamed c13cc57c48 feat: add support for individual consumable items in service items
- Introduced `consumables` field in `ServiceItem` to allow multiple individual items alongside inventory packages.
- Created `ServiceItemConsumable` entity to manage individual consumable items linked to a service.
- Updated `ServiceItemController` to handle CRUD operations for consumables.
- Enhanced `ServiceDetailPage` and `ServiceItemFormModal` to display and manage consumables.
- Added tests to ensure functionality for adding, updating, and validating consumables.
- Updated API documentation to reflect changes in service item structure and consumables.
2026-07-18 12:34:42 +03:30

341 lines
14 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Tests\ClinicService;
use App\Auth\Entity\User;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceSection;
use App\Doctor\Entity\Doctor;
use App\Inventory\Entity\InventoryItem;
use App\Inventory\Entity\InventoryPackage;
use App\Tests\ApiTestCase;
/**
* دو قابلیت صفحه‌ی جزئیات سرویس:
* ۱. اتصال اختیاری خدمت به یک پکیج کالای مصرفی (کالاهای مرتبط)
* ۲. تاریخچه‌ی تغییرات خدمت (service_item_audit_logs)
*/
class ServiceItemPackageAndAuditTest extends ApiTestCase
{
/** @return array{0: User, 1: Doctor, 2: ServiceSection} */
private function makeDoctorWithSection(): array
{
$owner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'دکتر تست پکیج');
$this->em->persist($doctor);
$this->em->flush();
$section = new ServiceSection('doctor', $doctor->getId(), 'تزریقات');
$this->em->persist($section);
$this->em->flush();
return [$owner, $doctor, $section];
}
private function makePackage(Doctor $doctor, string $title = 'پکیج سرم'): InventoryPackage
{
$package = new InventoryPackage('doctor', $doctor->getId(), $title);
$this->em->persist($package);
$this->em->flush();
return $package;
}
private function makeItem(ServiceSection $section, string $name = 'سرم ۵۰۰cc'): ServiceItem
{
$item = new ServiceItem($section, $name, 850_000);
$this->em->persist($item);
$this->em->flush();
return $item;
}
// ── کالاهای مرتبط ────────────────────────────────────────────────────────
public function testAttachingAPackageIsReflectedInTheDetailResponse(): void
{
[$owner, $doctor, $section] = $this->makeDoctorWithSection();
$package = $this->makePackage($doctor);
$item = $this->makeItem($section);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'inventory_package_uuid' => $package->getUuid(),
]);
$this->assertSame(200, $this->responseCode());
$body = $this->authJson('GET', '/api/v1/service-item/' . $item->getUuid(), $owner);
$this->assertSame($package->getUuid(), $body['data']['inventory_package_uuid']);
$this->assertSame('پکیج سرم', $body['data']['inventory_package_title']);
}
public function testDetachingWithNullClearsThePackage(): void
{
[$owner, $doctor, $section] = $this->makeDoctorWithSection();
$package = $this->makePackage($doctor);
$item = $this->makeItem($section);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'inventory_package_uuid' => $package->getUuid(),
]);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'inventory_package_uuid' => null,
]);
$body = $this->authJson('GET', '/api/v1/service-item/' . $item->getUuid(), $owner);
$this->assertNull($body['data']['inventory_package_uuid']);
}
public function testPackageOfAnotherTenantIsRejected(): void
{
[$owner, , $section] = $this->makeDoctorWithSection();
$item = $this->makeItem($section);
$stranger = $this->createUser(['ROLE_DOCTOR']);
$other = new Doctor($stranger, 'دکتر غریبه');
$this->em->persist($other);
$this->em->flush();
$foreignPackage = $this->makePackage($other, 'پکیج غریبه');
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'inventory_package_uuid' => $foreignPackage->getUuid(),
]);
$this->assertSame(422, $this->responseCode());
}
public function testUnknownPackageUuidIsRejected(): void
{
[$owner, , $section] = $this->makeDoctorWithSection();
$item = $this->makeItem($section);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'inventory_package_uuid' => 'no-such-package',
]);
$this->assertSame(422, $this->responseCode());
}
// ── کالاهای تکی (مکمل پکیج) ──────────────────────────────────────────────
private function makeInventoryItem(Doctor $doctor, string $name, int $price = 50_000): InventoryItem
{
$inventoryItem = new InventoryItem('doctor', $doctor->getId(), $name);
$inventoryItem->setPrice($price)->setStock(100);
$this->em->persist($inventoryItem);
$this->em->flush();
return $inventoryItem;
}
public function testAServiceCanHaveBothAPackageAndIndividualItems(): void
{
[$owner, $doctor, $section] = $this->makeDoctorWithSection();
$package = $this->makePackage($doctor);
$gauze = $this->makeInventoryItem($doctor, 'گاز استریل');
$syringe = $this->makeInventoryItem($doctor, 'سرنگ ۵cc');
$item = $this->makeItem($section);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'inventory_package_uuid' => $package->getUuid(),
'consumables' => [
['item_uuid' => $gauze->getUuid(), 'amount' => 2],
['item_uuid' => $syringe->getUuid(), 'amount' => 1],
],
]);
$this->assertSame(200, $this->responseCode());
$body = $this->authJson('GET', '/api/v1/service-item/' . $item->getUuid(), $owner);
$row = $body['data'];
$this->assertSame($package->getUuid(), $row['inventory_package_uuid'], 'پکیج باید کنار اقلام تکی بماند');
$this->assertCount(2, $row['consumables']);
$byName = array_column($row['consumables'], null, 'name');
$this->assertSame(2, $byName['گاز استریل']['amount']);
$this->assertSame(1, $byName['سرنگ ۵cc']['amount']);
$this->assertSame(50_000, $byName['گاز استریل']['price']);
}
public function testResendingConsumablesReplacesTheWholeList(): void
{
[$owner, $doctor, $section] = $this->makeDoctorWithSection();
$gauze = $this->makeInventoryItem($doctor, 'گاز استریل');
$syringe = $this->makeInventoryItem($doctor, 'سرنگ ۵cc');
$item = $this->makeItem($section);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'consumables' => [
['item_uuid' => $gauze->getUuid(), 'amount' => 2],
['item_uuid' => $syringe->getUuid(), 'amount' => 1],
],
]);
// فقط یک قلم با تعداد جدید ارسال می‌شود
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'consumables' => [['item_uuid' => $gauze->getUuid(), 'amount' => 5]],
]);
$body = $this->authJson('GET', '/api/v1/service-item/' . $item->getUuid(), $owner);
$this->assertCount(1, $body['data']['consumables']);
$this->assertSame('گاز استریل', $body['data']['consumables'][0]['name']);
$this->assertSame(5, $body['data']['consumables'][0]['amount']);
}
public function testEmptyConsumablesArrayClearsThem(): void
{
[$owner, $doctor, $section] = $this->makeDoctorWithSection();
$gauze = $this->makeInventoryItem($doctor, 'گاز استریل');
$item = $this->makeItem($section);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'consumables' => [['item_uuid' => $gauze->getUuid(), 'amount' => 2]],
]);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, ['consumables' => []]);
$body = $this->authJson('GET', '/api/v1/service-item/' . $item->getUuid(), $owner);
$this->assertSame([], $body['data']['consumables']);
}
public function testConsumableOfAnotherTenantIsRejected(): void
{
[$owner, , $section] = $this->makeDoctorWithSection();
$item = $this->makeItem($section);
$stranger = $this->createUser(['ROLE_DOCTOR']);
$other = new Doctor($stranger, 'دکتر غریبه');
$this->em->persist($other);
$this->em->flush();
$foreignItem = $this->makeInventoryItem($other, 'کالای غریبه');
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'consumables' => [['item_uuid' => $foreignItem->getUuid(), 'amount' => 1]],
]);
$this->assertSame(422, $this->responseCode());
}
public function testConsumablesCanBeSetAtCreationTime(): void
{
[$owner, $doctor, $section] = $this->makeDoctorWithSection();
$gauze = $this->makeInventoryItem($doctor, 'گاز استریل');
$created = $this->authJson('POST', '/api/v1/service-item', $owner, [
'section_uuid' => $section->getUuid(),
'name' => 'پانسمان',
'price_rials' => 200_000,
'consumables' => [['item_uuid' => $gauze->getUuid(), 'amount' => 3]],
]);
$this->assertSame(201, $this->responseCode());
$this->assertCount(1, $created['data']['consumables']);
$this->assertSame(3, $created['data']['consumables'][0]['amount']);
}
public function testChangingConsumablesIsLogged(): void
{
[$owner, $doctor, $section] = $this->makeDoctorWithSection();
$gauze = $this->makeInventoryItem($doctor, 'گاز استریل');
$item = $this->makeItem($section);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'consumables' => [['item_uuid' => $gauze->getUuid(), 'amount' => 2]],
]);
$body = $this->authJson('GET', '/api/v1/service-item/' . $item->getUuid() . '/audit-logs', $owner);
$byField = array_column($body['data'], null, 'field');
$this->assertArrayHasKey('consumables', $byField);
$this->assertNull($byField['consumables']['old_value']);
$this->assertSame('گاز استریل×2', $byField['consumables']['new_value']);
}
// ── لاگ تغییرات ──────────────────────────────────────────────────────────
public function testCreatingAServiceWritesACreateLog(): void
{
[$owner, , $section] = $this->makeDoctorWithSection();
$created = $this->authJson('POST', '/api/v1/service-item', $owner, [
'section_uuid' => $section->getUuid(),
'name' => 'خدمت تازه',
'price_rials' => 100_000,
]);
$this->assertSame(201, $this->responseCode());
$body = $this->authJson('GET', '/api/v1/service-item/' . $created['data']['uuid'] . '/audit-logs', $owner);
$this->assertSame(200, $this->responseCode());
$logs = $body['data'];
$this->assertCount(1, $logs);
$this->assertSame('create', $logs[0]['operation']);
$this->assertSame('خدمت تازه', $logs[0]['new_value']);
}
public function testEachChangedFieldGetsItsOwnLogRow(): void
{
[$owner, , $section] = $this->makeDoctorWithSection();
$item = $this->makeItem($section);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'name' => 'نام جدید',
'price_rials' => 990_000,
]);
$body = $this->authJson('GET', '/api/v1/service-item/' . $item->getUuid() . '/audit-logs', $owner);
$fields = array_column($body['data'], 'field');
sort($fields);
$this->assertSame(['name', 'price_rials'], $fields);
$byField = array_column($body['data'], null, 'field');
$this->assertSame('سرم ۵۰۰cc', $byField['name']['old_value']);
$this->assertSame('نام جدید', $byField['name']['new_value']);
$this->assertSame('850000', $byField['price_rials']['old_value']);
$this->assertSame('990000', $byField['price_rials']['new_value']);
}
public function testUnchangedFieldsAreNotLogged(): void
{
[$owner, , $section] = $this->makeDoctorWithSection();
$item = $this->makeItem($section);
// همان مقادیر فعلی دوباره ارسال می‌شوند
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'name' => 'سرم ۵۰۰cc',
'price_rials' => 850_000,
]);
$body = $this->authJson('GET', '/api/v1/service-item/' . $item->getUuid() . '/audit-logs', $owner);
$this->assertSame([], $body['data']);
}
public function testAttachingAPackageIsLogged(): void
{
[$owner, $doctor, $section] = $this->makeDoctorWithSection();
$package = $this->makePackage($doctor);
$item = $this->makeItem($section);
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
'inventory_package_uuid' => $package->getUuid(),
]);
$body = $this->authJson('GET', '/api/v1/service-item/' . $item->getUuid() . '/audit-logs', $owner);
$fields = array_column($body['data'], 'field');
$this->assertContains('inventory_package', $fields);
}
public function testAuditLogsOfAnotherTenantAreNotReadable(): void
{
[, , $section] = $this->makeDoctorWithSection();
$item = $this->makeItem($section);
$stranger = $this->createUser(['ROLE_DOCTOR']);
$this->em->persist(new Doctor($stranger, 'دکتر غریبه'));
$this->em->flush();
$this->authJson('GET', '/api/v1/service-item/' . $item->getUuid() . '/audit-logs', $stranger);
$this->assertSame(404, $this->responseCode());
}
}