import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import type { PrismaClient } from "@/generated/prisma/client" import { createTestCategory, 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 let AssetService: typeof import("@/services/asset.service").AssetService beforeAll(async () => { await startIntegrationTestDatabase() const prismaModule = await import("@/lib/prisma") const assetUseCases = await import("@/use-cases/asset.use-cases") const assetService = await import("@/services/asset.service") prisma = prismaModule.prisma createAssetUseCase = assetUseCases.createAssetUseCase updateAssetUseCase = assetUseCases.updateAssetUseCase AssetService = assetService.AssetService }) 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, previousStock: 0, newStock: 1, }) expect(movements[0].assetLines[0]).toMatchObject({ assetId: result.assetId, }) }) it("persists operational asset fields during create and update flows", async () => { const actor = await createTestUser(prisma) const item = await createTestItem(prisma, { stock: 0 }) const created = await createAssetUseCase({ actorId: actor.id, itemId: item.id, serialNumber: "ASSET-OPS-001", status: "AVAILABLE", assetTag: "IT-000777", manufacturer: "Lenovo", model: "ThinkPad X1", purchaseDate: new Date("2026-01-15T00:00:00.000Z"), purchasePrice: 1249.99, warrantyEndsAt: new Date("2028-01-15T00:00:00.000Z"), }) expect(created.success).toBe(true) if (!created.success) throw new Error("Expected asset creation success") const createdAsset = await prisma.asset.findUniqueOrThrow({ where: { id: created.assetId }, }) expect(createdAsset).toMatchObject({ assetTag: "IT-000777", manufacturer: "Lenovo", model: "ThinkPad X1", }) expect(createdAsset.purchaseDate?.toISOString()).toBe( "2026-01-15T00:00:00.000Z", ) expect(createdAsset.purchasePrice?.toString()).toBe("1249.99") expect(createdAsset.warrantyEndsAt?.toISOString()).toBe( "2028-01-15T00:00:00.000Z", ) const updated = await updateAssetUseCase({ actorId: actor.id, id: created.assetId, itemId: item.id, serialNumber: "ASSET-OPS-001", status: "BROKEN", assetTag: "IT-000778", manufacturer: "Dell", model: "Latitude 7420", purchaseDate: new Date("2026-02-01T00:00:00.000Z"), purchasePrice: 1499.5, warrantyEndsAt: new Date("2027-02-01T00:00:00.000Z"), }) expect(updated.success).toBe(true) const updatedAsset = await prisma.asset.findUniqueOrThrow({ where: { id: created.assetId }, }) expect(updatedAsset).toMatchObject({ assetTag: "IT-000778", manufacturer: "Dell", model: "Latitude 7420", status: "BROKEN", }) expect(updatedAsset.purchaseDate?.toISOString()).toBe( "2026-02-01T00:00:00.000Z", ) expect(updatedAsset.purchasePrice?.toString()).toBe("1499.5") expect(updatedAsset.warrantyEndsAt?.toISOString()).toBe( "2027-02-01T00:00:00.000Z", ) }) it("soft deletes assets and excludes them from active queries", async () => { const actor = await createTestUser(prisma) const item = await createTestItem(prisma, { stock: 0 }) const created = await createAssetUseCase({ actorId: actor.id, itemId: item.id, serialNumber: "ASSET-SOFT-DELETE-001", status: "AVAILABLE", }) expect(created.success).toBe(true) if (!created.success) throw new Error("Expected asset creation success") await AssetService.delete(created.assetId) const deletedAsset = await prisma.asset.findUniqueOrThrow({ where: { id: created.assetId }, }) expect(deletedAsset.deletedAt).toBeInstanceOf(Date) expect(await AssetService.findById(created.assetId)).toBeNull() expect(await AssetService.findAllAssetsCount()).toBe(0) expect(await AssetService.findAll()).toHaveLength(0) }) it("creates a SERIALIZED asset in AVAILABLE status without writing a stock line or incrementing stock", async () => { const actor = await createTestUser(prisma) const category = await createTestCategory(prisma) const item = await prisma.item.create({ data: { sku: "PHONE-SKU", name: "Phone", trackingType: "SERIALIZED", stock: 0, category: { connect: { id: category.id } }, }, }) const result = await createAssetUseCase({ actorId: actor.id, itemId: item.id, serialNumber: "SERIALIZED-AVAILABLE-001", status: "AVAILABLE", }) expect(result.success).toBe(true) if (!result.success) throw new Error("Expected asset creation success") const [updatedItem, movements] = await Promise.all([ prisma.item.findUniqueOrThrow({ where: { id: item.id } }), prisma.inventoryMovement.findMany({ include: { stockLines: true, assetLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }), ]) expect(updatedItem.stock).toBe(0) expect(movements).toHaveLength(1) expect(movements[0]).toMatchObject({ type: "RECEIPT", performedById: actor.id, }) expect(movements[0].stockLines).toEqual([]) expect(movements[0].assetLines).toHaveLength(1) expect(movements[0].assetLines[0]).toMatchObject({ assetId: result.assetId, }) }) it("updates a SERIALIZED asset from AVAILABLE to ASSIGNED without writing a stock line", async () => { const actor = await createTestUser(prisma) const person = await createTestPerson(prisma) const category = await createTestCategory(prisma) const item = await prisma.item.create({ data: { sku: "LAPTOP-SKU", name: "Laptop", trackingType: "SERIALIZED", stock: 0, category: { connect: { id: category.id } }, }, }) const created = await createAssetUseCase({ actorId: actor.id, itemId: item.id, serialNumber: "SERIALIZED-ASSIGN-001", status: "AVAILABLE", }) expect(created.success).toBe(true) if (!created.success) throw new Error("Expected asset creation success") const updated = await updateAssetUseCase({ actorId: actor.id, id: created.assetId, itemId: item.id, serialNumber: "SERIALIZED-ASSIGN-001", status: "ASSIGNED", personId: person.id, }) expect(updated.success).toBe(true) const [updatedItem, movements] = await Promise.all([ prisma.item.findUniqueOrThrow({ where: { id: item.id } }), prisma.inventoryMovement.findMany({ include: { stockLines: true, assetLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }), ]) expect(updatedItem.stock).toBe(0) expect(movements).toHaveLength(2) expect(movements[1].stockLines).toEqual([]) expect(movements[1].assetLines).toHaveLength(1) expect(movements[1].assetLines[0]).toMatchObject({ assetId: created.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).toEqual([]) expect(movements[0].assetLines).toHaveLength(1) expect(movements[0].assetLines[0]).toMatchObject({ assetId: result.assetId, }) }) it("rejects creating an assigned asset without a person", 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-ASSIGNED-NO-PERSON-001", status: "ASSIGNED", }) expect(result).toEqual({ success: false, errors: { personId: ["Person is required"] }, }) await expect(prisma.asset.count()).resolves.toBe(0) }) 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[0].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: 1, previousStock: 0, newStock: 1, }) expect(movements[0].assetLines[0]).toMatchObject({ assetId: created.assetId, previousStatus: null, newStatus: "AVAILABLE", }) expect(movements[1]).toMatchObject({ assignmentId: activeAssignment.id, performedById: actor.id, }) expect(movements[1].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: -1, previousStock: 1, newStock: 0, }) expect(movements[1].assetLines[0]).toMatchObject({ assetId: created.assetId, previousStatus: "AVAILABLE", newStatus: "ASSIGNED", }) expect(movements[2]).toMatchObject({ assignmentId: activeAssignment.id, performedById: actor.id, }) expect(movements[2].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: 1, previousStock: 0, newStock: 1, }) expect(movements[2].assetLines[0]).toMatchObject({ assetId: created.assetId, previousStatus: "ASSIGNED", newStatus: "AVAILABLE", }) }) it("sets previousStatus to null on first asset status (create AVAILABLE)", 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-CREATE-PREV-001", status: "AVAILABLE", }) expect(result.success).toBe(true) if (!result.success) throw new Error("Expected asset creation success") const movements = await prisma.inventoryMovement.findMany({ include: { assetLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }) expect(movements).toHaveLength(1) expect(movements[0].assetLines[0]).toMatchObject({ assetId: result.assetId, previousStatus: null, newStatus: "AVAILABLE", }) }) it("records previousStatus AVAILABLE on AVAILABLE to BROKEN transition", async () => { const actor = await createTestUser(prisma) const item = await createTestItem(prisma, { stock: 0 }) const created = await createAssetUseCase({ actorId: actor.id, itemId: item.id, serialNumber: "ASSET-BROKEN-PREV-001", status: "AVAILABLE", }) expect(created.success).toBe(true) if (!created.success) throw new Error("Expected asset creation success") const updated = await updateAssetUseCase({ actorId: actor.id, id: created.assetId, itemId: item.id, serialNumber: "ASSET-BROKEN-PREV-001", status: "BROKEN", }) expect(updated.success).toBe(true) const movements = await prisma.inventoryMovement.findMany({ include: { assetLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }) expect(movements).toHaveLength(2) expect(movements[1].assetLines[0]).toMatchObject({ assetId: created.assetId, previousStatus: "AVAILABLE", newStatus: "BROKEN", }) }) it("writes two AssetMovementLines with correct previousStatus on itemChanged AVAILABLE to AVAILABLE", async () => { const actor = await createTestUser(prisma) const sourceItem = await createTestItem(prisma, { stock: 1 }) const targetItem = await createTestItem(prisma, { stock: 0 }) const created = await createAssetUseCase({ actorId: actor.id, itemId: sourceItem.id, serialNumber: "ASSET-ITEMCHG-001", status: "AVAILABLE", }) expect(created.success).toBe(true) if (!created.success) throw new Error("Expected asset creation success") const updated = await updateAssetUseCase({ actorId: actor.id, id: created.assetId, itemId: targetItem.id, serialNumber: "ASSET-ITEMCHG-001", status: "AVAILABLE", }) expect(updated.success).toBe(true) const movements = await prisma.inventoryMovement.findMany({ include: { assetLines: true, stockLines: true }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], }) const itemChangedMovements = movements.slice(1) expect(itemChangedMovements).toHaveLength(2) const allAssetLines = itemChangedMovements.flatMap( (movement) => movement.assetLines, ) expect(allAssetLines).toHaveLength(2) for (const line of allAssetLines) { expect(line).toMatchObject({ assetId: created.assetId, previousStatus: "AVAILABLE", newStatus: "AVAILABLE", }) } }) it("rejects updating an asset to assigned without a person", async () => { const actor = await createTestUser(prisma) const item = await createTestItem(prisma, { stock: 0 }) const created = await createAssetUseCase({ actorId: actor.id, itemId: item.id, serialNumber: "ASSET-UPDATE-ASSIGNED-NO-PERSON-001", status: "AVAILABLE", }) expect(created.success).toBe(true) if (!created.success) throw new Error("Expected asset creation success") const result = await updateAssetUseCase({ actorId: actor.id, id: created.assetId, itemId: item.id, serialNumber: "ASSET-UPDATE-ASSIGNED-NO-PERSON-001", status: "ASSIGNED", }) expect(result).toEqual({ success: false, errors: { personId: ["Person is required"] }, }) const asset = await prisma.asset.findUniqueOrThrow({ where: { id: created.assetId }, }) expect(asset.status).toBe("AVAILABLE") }) 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, }) }) })