Files
clinicpro/tests/ClinicService/ServiceItemPackageAndAuditTest.php
T
hamed c4a661b542 feat: add optional inventory package association to service items and implement audit logging
- Added `inventory_package_uuid` and `inventory_package_title` fields to the `ServiceItem` interface.
- Updated API documentation to reflect new fields in service item responses.
- Implemented methods in `ClinicServiceController` to handle inventory package associations.
- Created `ServiceItemAuditLog` entity and repository for tracking changes to service items.
- Added functionality to log changes to service items, including inventory package associations.
- Implemented tests for attaching/detaching inventory packages and auditing changes.
- Created database migrations for new fields and audit log table.
2026-07-18 12:26:15 +03:30

206 lines
7.9 KiB
PHP

<?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\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());
}
// ── لاگ تغییرات ──────────────────────────────────────────────────────────
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());
}
}