import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import type { PrismaClient } from "@/generated/prisma/client" import { createTestItem, createTestPerson, createTestUser, } from "../helpers/factories" import { resetIntegrationTestDatabase, startIntegrationTestDatabase, stopIntegrationTestDatabase, } from "../helpers/test-db" let prisma: PrismaClient let createAssetUseCase: typeof import("@/use-cases/asset.use-cases").createAssetUseCase let updateAssetUseCase: typeof import("@/use-cases/asset.use-cases").updateAssetUseCase beforeAll(async () => { await startIntegrationTestDatabase() const prismaModule = await import("@/lib/prisma") const assetUseCases = await import("@/use-cases/asset.use-cases") prisma = prismaModule.prisma createAssetUseCase = assetUseCases.createAssetUseCase updateAssetUseCase = assetUseCases.updateAssetUseCase }) beforeEach(async () => { await resetIntegrationTestDatabase(prisma) }) afterAll(async () => { await prisma?.$disconnect() await stopIntegrationTestDatabase() }) describe("asset use-cases", () => { it("creates an available asset, increments item stock, and records an IN movement", async () => { const actor = await createTestUser(prisma) const item = await createTestItem(prisma, { stock: 0 }) const result = await createAssetUseCase({ actorId: actor.id, itemId: item.id, serialNumber: "ASSET-AVAILABLE-001", deliveryNote: "DN-001", status: "AVAILABLE", notes: "Ready to assign", }) expect(result.success).toBe(true) if (!result.success) throw new Error("Expected asset creation success") const [asset, updatedItem, movements] = await Promise.all([ prisma.asset.findUniqueOrThrow({ where: { id: result.assetId } }), prisma.item.findUniqueOrThrow({ where: { id: item.id } }), prisma.inventoryMovement.findMany({ include: { stockLines: true, assetLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }), ]) expect(asset).toMatchObject({ itemId: item.id, serialNumber: "ASSET-AVAILABLE-001", deliveryNote: "DN-001", status: "AVAILABLE", notes: "Ready to assign", }) expect(updatedItem.stock).toBe(1) expect(movements).toHaveLength(1) expect(movements[0]).toMatchObject({ type: "RECEIPT", performedById: actor.id, }) expect(movements[0].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: 1 }) expect(movements[0].assetLines[0]).toMatchObject({ assetId: result.assetId }) }) it("creates an assigned asset with assignment and ASSIGNMENT movement", async () => { const actor = await createTestUser(prisma) const person = await createTestPerson(prisma) const item = await createTestItem(prisma, { stock: 0 }) const result = await createAssetUseCase({ actorId: actor.id, itemId: item.id, serialNumber: "ASSET-ASSIGNED-001", status: "ASSIGNED", personId: person.id, }) expect(result.success).toBe(true) if (!result.success) throw new Error("Expected asset creation success") const [asset, updatedItem, assignment, movements] = await Promise.all([ prisma.asset.findUniqueOrThrow({ where: { id: result.assetId } }), prisma.item.findUniqueOrThrow({ where: { id: item.id } }), prisma.assignment.findFirstOrThrow({ where: { status: "OPEN", assetLines: { some: { assetId: result.assetId, returnedAt: null } }, }, include: { assetLines: true }, }), prisma.inventoryMovement.findMany({ include: { stockLines: true, assetLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }), ]) expect(asset).toMatchObject({ itemId: item.id, serialNumber: "ASSET-ASSIGNED-001", status: "ASSIGNED", }) expect(updatedItem.stock).toBe(0) expect(assignment).toMatchObject({ personId: person.id, createdById: actor.id, closedAt: null, }) expect(assignment.assetLines[0]).toMatchObject({ assetId: result.assetId }) expect(movements).toHaveLength(1) expect(movements[0]).toMatchObject({ type: "ASSIGNMENT", assignmentId: assignment.id, performedById: actor.id, }) expect(movements[0].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: -1 }) expect(movements[0].assetLines[0]).toMatchObject({ assetId: result.assetId }) }) it("moves an available asset to assigned and back to available", async () => { const actor = await createTestUser(prisma) const person = await createTestPerson(prisma) const item = await createTestItem(prisma, { stock: 0 }) const created = await createAssetUseCase({ actorId: actor.id, itemId: item.id, serialNumber: "ASSET-LIFECYCLE-001", status: "AVAILABLE", }) expect(created.success).toBe(true) if (!created.success) throw new Error("Expected asset creation success") await expect( updateAssetUseCase({ actorId: actor.id, id: created.assetId, itemId: item.id, serialNumber: "ASSET-LIFECYCLE-001", status: "ASSIGNED", personId: person.id, }), ).resolves.toEqual({ success: true }) const [assignedAsset, assignedItem, activeAssignment] = await Promise.all([ prisma.asset.findUniqueOrThrow({ where: { id: created.assetId } }), prisma.item.findUniqueOrThrow({ where: { id: item.id } }), prisma.assignment.findFirstOrThrow({ where: { status: "OPEN", assetLines: { some: { assetId: created.assetId, returnedAt: null } }, }, include: { assetLines: true }, }), ]) expect(assignedAsset.status).toBe("ASSIGNED") expect(assignedItem.stock).toBe(0) expect(activeAssignment).toMatchObject({ personId: person.id, }) expect(activeAssignment.assetLines[0]).toMatchObject({ assetId: created.assetId }) await expect( updateAssetUseCase({ actorId: actor.id, id: created.assetId, itemId: item.id, serialNumber: "ASSET-LIFECYCLE-001", status: "AVAILABLE", }), ).resolves.toEqual({ success: true }) const [availableAsset, availableItem, returnedAssignment, movements] = await Promise.all([ prisma.asset.findUniqueOrThrow({ where: { id: created.assetId } }), prisma.item.findUniqueOrThrow({ where: { id: item.id } }), prisma.assignment.findUniqueOrThrow({ where: { id: activeAssignment.id }, }), prisma.inventoryMovement.findMany({ include: { stockLines: true, assetLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }), ]) expect(availableAsset.status).toBe("AVAILABLE") expect(availableItem.stock).toBe(1) expect(returnedAssignment.closedAt).toBeInstanceOf(Date) expect(returnedAssignment).toMatchObject({ personId: person.id, status: "RETURNED", }) expect(movements).toHaveLength(3) expect(movements.map((movement) => movement.type)).toEqual([ "RECEIPT", "ASSIGNMENT", "RETURN", ]) expect(movements[1]).toMatchObject({ assignmentId: activeAssignment.id, performedById: actor.id, }) expect(movements[1].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: -1 }) expect(movements[1].assetLines[0]).toMatchObject({ assetId: created.assetId }) expect(movements[2]).toMatchObject({ assignmentId: activeAssignment.id, performedById: actor.id, }) expect(movements[2].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: 1 }) expect(movements[2].assetLines[0]).toMatchObject({ assetId: created.assetId }) }) it("returns an active assignment without restoring stock when an assigned asset moves to a terminal status", async () => { const actor = await createTestUser(prisma) const person = await createTestPerson(prisma) const item = await createTestItem(prisma, { stock: 0 }) const created = await createAssetUseCase({ actorId: actor.id, itemId: item.id, serialNumber: "ASSET-BROKEN-001", status: "ASSIGNED", personId: person.id, }) expect(created.success).toBe(true) if (!created.success) throw new Error("Expected asset creation success") const activeAssignment = await prisma.assignment.findFirstOrThrow({ where: { status: "OPEN", assetLines: { some: { assetId: created.assetId, returnedAt: null } }, }, }) await expect( updateAssetUseCase({ actorId: actor.id, id: created.assetId, itemId: item.id, serialNumber: "ASSET-BROKEN-001", status: "BROKEN", }), ).resolves.toEqual({ success: true }) const [asset, itemAfterUpdate, returnedAssignment, movements] = await Promise.all([ prisma.asset.findUniqueOrThrow({ where: { id: created.assetId } }), prisma.item.findUniqueOrThrow({ where: { id: item.id } }), prisma.assignment.findUniqueOrThrow({ where: { id: activeAssignment.id }, }), prisma.inventoryMovement.findMany({ include: { stockLines: true, assetLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }), ]) expect(asset.status).toBe("BROKEN") expect(itemAfterUpdate.stock).toBe(0) expect(returnedAssignment.closedAt).toBeInstanceOf(Date) expect(movements).toHaveLength(2) expect(movements.map((movement) => movement.type)).toEqual([ "ASSIGNMENT", "RETURN", ]) expect(movements[1]).toMatchObject({ type: "RETURN", assignmentId: activeAssignment.id, performedById: actor.id, }) expect(movements[1].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: 1 }) expect(movements[1].assetLines[0]).toMatchObject({ assetId: created.assetId }) }) })