import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import type { PrismaClient } from "@/generated/prisma/client" import { createTestCategory, createTestUser } from "../helpers/factories" import { resetIntegrationTestDatabase, startIntegrationTestDatabase, stopIntegrationTestDatabase, } from "../helpers/test-db" let prisma: PrismaClient let createItemUseCase: typeof import("@/use-cases/item.use-cases").createItemUseCase let deleteItemUseCase: typeof import("@/use-cases/item.use-cases").deleteItemUseCase let updateItemUseCase: typeof import("@/use-cases/item.use-cases").updateItemUseCase beforeAll(async () => { await startIntegrationTestDatabase() const prismaModule = await import("@/lib/prisma") const itemUseCases = await import("@/use-cases/item.use-cases") prisma = prismaModule.prisma createItemUseCase = itemUseCases.createItemUseCase deleteItemUseCase = itemUseCases.deleteItemUseCase updateItemUseCase = itemUseCases.updateItemUseCase }) beforeEach(async () => { await resetIntegrationTestDatabase(prisma) }) afterAll(async () => { await prisma?.$disconnect() await stopIntegrationTestDatabase() }) describe("item use-cases", () => { it("creates an item with operational fields and records an IN movement", async () => { const actor = await createTestUser(prisma) const category = await createTestCategory(prisma) const result = await createItemUseCase({ actorId: actor.id, name: "Laptop", categoryId: category.id, stock: 3, trackingType: "QUANTITY", status: "ACTIVE", minStock: 1, targetStock: 6, }) expect(result).toEqual({ success: true }) const item = await prisma.item.findUnique({ where: { sku: "LAPTOP" }, include: { stockMovementLines: { include: { movement: true } } }, }) expect(item).toMatchObject({ name: "Laptop", categoryId: category.id, stock: 3, trackingType: "QUANTITY", status: "ACTIVE", minStock: 1, targetStock: 6, deletedAt: null, }) expect(item?.stockMovementLines).toHaveLength(1) expect(item?.stockMovementLines[0]).toMatchObject({ stockDelta: 3, }) expect(item?.stockMovementLines[0]?.movement).toMatchObject({ type: "RECEIPT", performedById: actor.id, }) }) it("generates unique skus for different names with the same normalized base", async () => { const actor = await createTestUser(prisma) const category = await createTestCategory(prisma) await createItemUseCase({ actorId: actor.id, name: "Item A!", categoryId: category.id, stock: 0, }) const secondCreate = await createItemUseCase({ actorId: actor.id, name: "Item A?", categoryId: category.id, stock: 0, }) expect(secondCreate).toEqual({ success: true }) const items = await prisma.item.findMany({ where: { categoryId: category.id }, orderBy: { sku: "asc" }, select: { sku: true, name: true }, }) expect(items).toEqual([ { sku: "ITEM-A", name: "Item A!" }, { sku: "ITEM-A-2", name: "Item A?" }, ]) }) it("updates operational item fields without changing the sku", async () => { const actor = await createTestUser(prisma) const category = await createTestCategory(prisma) const createResult = await createItemUseCase({ actorId: actor.id, name: "Monitor", categoryId: category.id, stock: 1, }) expect(createResult).toEqual({ success: true }) const item = await prisma.item.findUniqueOrThrow({ where: { sku: "MONITOR" }, }) const updateResult = await updateItemUseCase({ actorId: actor.id, id: item.id, name: "Monitor", categoryId: category.id, stock: 0, trackingType: "SERIALIZED", status: "DISCONTINUED", minStock: 2, targetStock: 8, }) expect(updateResult).toEqual({ success: true }) const updatedItem = await prisma.item.findUniqueOrThrow({ where: { id: item.id }, }) expect(updatedItem).toMatchObject({ sku: "MONITOR", stock: 0, trackingType: "SERIALIZED", status: "DISCONTINUED", minStock: 2, targetStock: 8, }) }) it("rejects duplicate item names", async () => { const actor = await createTestUser(prisma) const category = await createTestCategory(prisma) await createItemUseCase({ actorId: actor.id, name: "Monitor", categoryId: category.id, stock: 0, }) const duplicate = await createItemUseCase({ actorId: actor.id, name: "Monitor", categoryId: category.id, stock: 0, }) expect(duplicate).toEqual({ success: false, errors: { name: ["An item with this name already exists"], }, }) }) it("writes an ISSUE movement when a QUANTITY item stock decreases to zero", async () => { const actor = await createTestUser(prisma) const category = await createTestCategory(prisma) const created = await createItemUseCase({ actorId: actor.id, name: "Cable", categoryId: category.id, stock: 1, }) expect(created).toEqual({ success: true }) const item = await prisma.item.findUniqueOrThrow({ where: { sku: "CABLE" }, }) const updated = await updateItemUseCase({ actorId: actor.id, id: item.id, name: "Cable", categoryId: category.id, stock: 0, }) expect(updated).toEqual({ success: true }) const refreshedItem = await prisma.item.findUniqueOrThrow({ where: { id: item.id }, }) expect(refreshedItem.stock).toBe(0) const movements = await prisma.inventoryMovement.findMany({ where: { stockLines: { some: { itemId: item.id } } }, include: { stockLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }) expect(movements).toHaveLength(2) expect(movements[1]).toMatchObject({ type: "ISSUE", performedById: actor.id, }) expect(movements[1].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: -1, previousStock: 1, newStock: 0, }) }) it("writes an ADJUSTMENT movement with INVENTORY_CORRECTION reason when QUANTITY item decreases with reason", async () => { const actor = await createTestUser(prisma) const category = await createTestCategory(prisma) const created = await createItemUseCase({ actorId: actor.id, name: "Mouse", categoryId: category.id, stock: 5, }) expect(created).toEqual({ success: true }) const item = await prisma.item.findUniqueOrThrow({ where: { sku: "MOUSE" }, }) const updated = await updateItemUseCase({ actorId: actor.id, id: item.id, name: "Mouse", categoryId: category.id, stock: 4, reason: "INVENTORY_CORRECTION", }) expect(updated).toEqual({ success: true }) const refreshedItem = await prisma.item.findUniqueOrThrow({ where: { id: item.id }, }) expect(refreshedItem.stock).toBe(4) const movements = await prisma.inventoryMovement.findMany({ where: { stockLines: { some: { itemId: item.id } } }, include: { stockLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }) expect(movements).toHaveLength(2) expect(movements[1]).toMatchObject({ type: "ADJUSTMENT", reason: "INVENTORY_CORRECTION", performedById: actor.id, }) expect(movements[1].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: -1, previousStock: 5, newStock: 4, }) }) it("blocks deleting items with stock and soft deletes empty items", async () => { const actor = await createTestUser(prisma) const category = await createTestCategory(prisma) await createItemUseCase({ actorId: actor.id, name: "Keyboard", categoryId: category.id, stock: 2, }) const stockedItem = await prisma.item.findUniqueOrThrow({ where: { sku: "KEYBOARD" }, }) await expect(deleteItemUseCase(stockedItem.id)).resolves.toEqual({ success: false, errors: { id: ["Item has stock, you cannot delete it"] }, }) await createItemUseCase({ actorId: actor.id, name: "Mouse", categoryId: category.id, stock: 0, }) const emptyItem = await prisma.item.findUniqueOrThrow({ where: { sku: "MOUSE" }, }) await expect(deleteItemUseCase(emptyItem.id)).resolves.toEqual({ success: true, }) const deletedItem = await prisma.item.findUniqueOrThrow({ where: { id: emptyItem.id }, }) expect(deletedItem.deletedAt).toBeInstanceOf(Date) }) })