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()); } }