feat(assets): add asset metadata views and enforce assignment transitions
This commit is contained in:
@@ -14,16 +14,19 @@ import {
|
||||
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 () => {
|
||||
@@ -74,8 +77,110 @@ describe("asset use-cases", () => {
|
||||
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 })
|
||||
expect(movements[0].stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
stockDelta: 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 an assigned asset with assignment and ASSIGNMENT movement", async () => {
|
||||
@@ -128,8 +233,31 @@ describe("asset use-cases", () => {
|
||||
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 })
|
||||
expect(movements[0].stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
stockDelta: -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 () => {
|
||||
@@ -175,7 +303,9 @@ describe("asset use-cases", () => {
|
||||
expect(activeAssignment).toMatchObject({
|
||||
personId: person.id,
|
||||
})
|
||||
expect(activeAssignment.assetLines[0]).toMatchObject({ assetId: created.assetId })
|
||||
expect(activeAssignment.assetLines[0]).toMatchObject({
|
||||
assetId: created.assetId,
|
||||
})
|
||||
|
||||
await expect(
|
||||
updateAssetUseCase({
|
||||
@@ -217,14 +347,57 @@ describe("asset use-cases", () => {
|
||||
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[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 })
|
||||
expect(movements[2].stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
stockDelta: 1,
|
||||
})
|
||||
expect(movements[2].assetLines[0]).toMatchObject({
|
||||
assetId: created.assetId,
|
||||
})
|
||||
})
|
||||
|
||||
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 () => {
|
||||
@@ -286,7 +459,12 @@ describe("asset use-cases", () => {
|
||||
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[1].stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
stockDelta: 1,
|
||||
})
|
||||
expect(movements[1].assetLines[0]).toMatchObject({
|
||||
assetId: created.assetId,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user