createUser(['ROLE_DOCTOR']); $doctor = new Doctor($user, 'دکتر'); $this->em->persist($doctor); $this->em->flush(); return [$user, $doctor]; } // ── Items ──────────────────────────────────────────────────────────────── public function testItemCreateListUpdateDelete(): void { [$user] = $this->doctorUser(); $created = $this->authJson('POST', '/api/v1/inventory-item', $user, [ 'name' => 'دستکش', 'unit' => 'عدد', 'price' => 250000, 'stock' => 150, 'alertThreshold' => 20, ]); self::assertSame(201, $this->responseCode()); self::assertSame('دستکش', $created['data']['name']); self::assertSame('in_stock', $created['data']['status']); $uuid = $created['data']['uuid']; $list = $this->authJson('GET', '/api/v1/inventory-items', $user); self::assertSame(200, $this->responseCode()); self::assertCount(1, $list['data']['items']); self::assertSame(1, $list['data']['stats']['total']); self::assertSame(1, $list['data']['stats']['inStock']); // drop stock below threshold → low_stock $this->authJson('PATCH', '/api/v1/inventory-item/' . $uuid, $user, ['stock' => 5]); self::assertSame(200, $this->responseCode()); $afterPatch = $this->authJson('GET', '/api/v1/inventory-items', $user); self::assertSame('low_stock', $afterPatch['data']['items'][0]['status']); self::assertSame(1, $afterPatch['data']['stats']['low']); $this->authJson('DELETE', '/api/v1/inventory-item/' . $uuid, $user); self::assertSame(200, $this->responseCode()); $after = $this->authJson('GET', '/api/v1/inventory-items', $user); self::assertCount(0, $after['data']['items']); } public function testStatusDerivation(): void { [$user] = $this->doctorUser(); // zero stock → out_of_stock, regardless of threshold $out = $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'تمام‌شده', 'stock' => 0, 'alertThreshold' => 5]); self::assertSame('out_of_stock', $out['data']['status']); // stock above threshold → in_stock $in = $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'موجود', 'stock' => 100, 'alertThreshold' => 10]); self::assertSame('in_stock', $in['data']['status']); } public function testItemRejectsEmptyName(): void { [$user] = $this->doctorUser(); $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => '']); self::assertSame(422, $this->responseCode()); } public function testEmptyStateReturnsZeroStats(): void { [$user] = $this->doctorUser(); $list = $this->authJson('GET', '/api/v1/inventory-items', $user); self::assertSame(200, $this->responseCode()); self::assertCount(0, $list['data']['items']); self::assertSame( ['total' => 0, 'low' => 0, 'inStock' => 0, 'outOfStock' => 0], $list['data']['stats'] ); } public function testCategoriesReturnsDistinctUsedCategories(): void { [$user] = $this->doctorUser(); $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'a', 'category' => 'دارو']); $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'b', 'category' => 'دارو']); $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'c', 'category' => 'لوازم آزمایشگاهی']); $cats = $this->authJson('GET', '/api/v1/inventory-categories', $user); self::assertSame(200, $this->responseCode()); self::assertCount(2, $cats['data']); self::assertContains('دارو', $cats['data']); self::assertContains('لوازم آزمایشگاهی', $cats['data']); } public function testMetaReturnsUnitAndCategoryLists(): void { [$user] = $this->doctorUser(); $meta = $this->authJson('GET', '/api/v1/inventory-meta', $user); self::assertSame(200, $this->responseCode()); self::assertContains('عدد', $meta['data']['units']); self::assertContains('سی‌سی', $meta['data']['units']); self::assertContains('دارو', $meta['data']['categories']); self::assertContains('سایر', $meta['data']['categories']); } public function testCreateStoresValidUnitAndCategory(): void { [$user] = $this->doctorUser(); $item = $this->authJson('POST', '/api/v1/inventory-item', $user, [ 'name' => 'سرنگ', 'unit' => 'بسته', 'category' => 'لوازم مصرفی و تزریقات', ]); self::assertSame(201, $this->responseCode()); self::assertSame('بسته', $item['data']['unit']); self::assertSame('لوازم مصرفی و تزریقات', $item['data']['category']); } public function testCreateRejectsInvalidUnit(): void { [$user] = $this->doctorUser(); $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'x', 'unit' => 'واحد‌جعلی']); self::assertSame(422, $this->responseCode()); } public function testCreateRejectsInvalidCategory(): void { [$user] = $this->doctorUser(); $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'x', 'category' => 'دسته‌جعلی']); self::assertSame(422, $this->responseCode()); } public function testEmptyUnitFallsBackToDefault(): void { [$user] = $this->doctorUser(); $item = $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'x', 'unit' => '']); self::assertSame(201, $this->responseCode()); self::assertSame('عدد', $item['data']['unit']); } public function testCannotTouchAnotherTenantsItem(): void { [$ownerA] = $this->doctorUser(); $created = $this->authJson('POST', '/api/v1/inventory-item', $ownerA, ['name' => 'مال A']); $uuid = $created['data']['uuid']; [$ownerB] = $this->doctorUser(); $this->authJson('PATCH', '/api/v1/inventory-item/' . $uuid, $ownerB, ['name' => 'دزدی']); self::assertSame(404, $this->responseCode()); $this->authJson('DELETE', '/api/v1/inventory-item/' . $uuid, $ownerB); self::assertSame(404, $this->responseCode()); } // ── Packages ───────────────────────────────────────────────────────────── public function testPackageCreateComputesTotalAndAvailability(): void { [$user] = $this->doctorUser(); $gel = $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'ژل', 'price' => 1200000, 'stock' => 10])['data']['uuid']; $glove = $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'دستکش', 'price' => 500000, 'stock' => 1])['data']['uuid']; // 2×ژل (stock 10, ok) + 3×دستکش (stock 1, NOT enough) → unavailable $pkg = $this->authJson('POST', '/api/v1/inventory-package', $user, [ 'title' => 'پکیج یک', 'items' => [ ['itemUuid' => $gel, 'amount' => 2], ['itemUuid' => $glove, 'amount' => 3], ], ]); self::assertSame(201, $this->responseCode()); self::assertSame(2 * 1200000 + 3 * 500000, $pkg['data']['total']); self::assertFalse($pkg['data']['available']); self::assertCount(2, $pkg['data']['items']); // list reflects the same package $list = $this->authJson('GET', '/api/v1/inventory-packages', $user); self::assertCount(1, $list['data']); self::assertSame('پکیج یک', $list['data'][0]['title']); } public function testPackageUpdateReplacesItemsAndDelete(): void { [$user] = $this->doctorUser(); $a = $this->authJson('POST', '/api/v1/inventory-item', $user, ['name' => 'A', 'price' => 1000, 'stock' => 50])['data']['uuid']; $uuid = $this->authJson('POST', '/api/v1/inventory-package', $user, [ 'title' => 'p', 'items' => [['itemUuid' => $a, 'amount' => 1]], ])['data']['uuid']; $updated = $this->authJson('PATCH', '/api/v1/inventory-package/' . $uuid, $user, [ 'title' => 'p2', 'items' => [['itemUuid' => $a, 'amount' => 5]], ]); self::assertSame(200, $this->responseCode()); self::assertSame('p2', $updated['data']['title']); self::assertCount(1, $updated['data']['items']); self::assertSame(5, $updated['data']['items'][0]['amount']); self::assertSame(5000, $updated['data']['total']); $this->authJson('DELETE', '/api/v1/inventory-package/' . $uuid, $user); self::assertSame(200, $this->responseCode()); $after = $this->authJson('GET', '/api/v1/inventory-packages', $user); self::assertCount(0, $after['data']); } public function testPackageSkipsForeignItemReferences(): void { [$ownerA] = $this->doctorUser(); $foreign = $this->authJson('POST', '/api/v1/inventory-item', $ownerA, ['name' => 'خارجی', 'price' => 999, 'stock' => 5])['data']['uuid']; [$ownerB] = $this->doctorUser(); $mine = $this->authJson('POST', '/api/v1/inventory-item', $ownerB, ['name' => 'مال من', 'price' => 100, 'stock' => 5])['data']['uuid']; // package for B referencing A's item → foreign line dropped, only B's stays $pkg = $this->authJson('POST', '/api/v1/inventory-package', $ownerB, [ 'title' => 'mix', 'items' => [ ['itemUuid' => $foreign, 'amount' => 1], ['itemUuid' => $mine, 'amount' => 2], ], ]); self::assertSame(201, $this->responseCode()); self::assertCount(1, $pkg['data']['items']); self::assertSame($mine, $pkg['data']['items'][0]['itemUuid']); self::assertSame(200, $pkg['data']['total']); } public function testPackageRejectsEmptyTitle(): void { [$user] = $this->doctorUser(); $this->authJson('POST', '/api/v1/inventory-package', $user, ['title' => '']); self::assertSame(422, $this->responseCode()); } }