From 1142855578ea4e5f3addd6fb3f982a753556875c Mon Sep 17 00:00:00 2001 From: Asis Ferrer Date: Thu, 25 Jun 2026 03:25:47 +0200 Subject: [PATCH] feat(assets): thread previousStatus through movement writes --- src/services/movement.service.ts | 5 +- src/use-cases/asset.use-cases.ts | 9 ++ .../use-cases/asset.use-cases.test.ts | 119 ++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/src/services/movement.service.ts b/src/services/movement.service.ts index 14639ab..4819953 100644 --- a/src/services/movement.service.ts +++ b/src/services/movement.service.ts @@ -1,4 +1,4 @@ -import type { InventoryMovementType, Prisma } from "@/generated/prisma/client" +import type { AssetStatus, InventoryMovementType, Prisma } from "@/generated/prisma/client" import { paginate } from "@/lib/paginate" import prisma from "@/lib/prisma" import type { CreateMovementFormType } from "@/schemas/movement.schema" @@ -115,6 +115,7 @@ function getMovementPerson(movement: { type CreateMovementServiceInput = CreateMovementFormType & { userId: string stockDeltaSign?: 1 | -1 + previousStatus?: AssetStatus | null } export const MovementService = { @@ -190,6 +191,7 @@ export const MovementService = { type, userId, stockDeltaSign, + previousStatus, ...rest } = data const sign = stockDeltaSign ?? stockDeltaSignMap[type] @@ -228,6 +230,7 @@ export const MovementService = { ? { create: { assetId, + previousStatus: previousStatus ?? null, newStatus: asset.status, }, } diff --git a/src/use-cases/asset.use-cases.ts b/src/use-cases/asset.use-cases.ts index 7bf8f94..9995f56 100644 --- a/src/use-cases/asset.use-cases.ts +++ b/src/use-cases/asset.use-cases.ts @@ -186,6 +186,7 @@ export async function createAssetUseCase( personId: createdAssignment?.personId || undefined, assignmentId: createdAssignment?.id, userId: actorId, + previousStatus: null, }, tx, ) @@ -352,6 +353,7 @@ export async function updateAssetUseCase( personId: activeAssignment.personId || undefined, assignmentId: activeAssignment.id, userId: actorId, + previousStatus: transition.previousStatus, }, tx, ) @@ -384,6 +386,7 @@ export async function updateAssetUseCase( : "ADJUSTMENT", details: `Status changed from ${transition.previousStatus} to ${transition.nextStatus}`, userId: actorId, + previousStatus: transition.previousStatus, }, tx, ) @@ -408,6 +411,7 @@ export async function updateAssetUseCase( type: "OUT", details: `Asset moved from item ${transition.previousItemId} to ${transition.nextItemId}`, userId: actorId, + previousStatus: transition.previousStatus, }, tx, ) @@ -420,6 +424,7 @@ export async function updateAssetUseCase( type: "IN", details: `Asset moved from item ${transition.previousItemId} to ${transition.nextItemId}`, userId: actorId, + previousStatus: transition.previousStatus, }, tx, ) @@ -444,6 +449,7 @@ export async function updateAssetUseCase( type: "OUT", details: `Asset assigned from item ${transition.previousItemId} to item ${transition.nextItemId}`, userId: actorId, + previousStatus: transition.previousStatus, }, tx, ) @@ -463,6 +469,7 @@ export async function updateAssetUseCase( personId: activeAssignment.personId || undefined, assignmentId: activeAssignment.id, userId: actorId, + previousStatus: transition.previousStatus, }, tx, ) @@ -476,6 +483,7 @@ export async function updateAssetUseCase( personId: transition.nextPersonId, assignmentId: activeAssignment.id, userId: actorId, + previousStatus: transition.previousStatus, }, tx, ) @@ -527,6 +535,7 @@ export async function updateAssetUseCase( personId: transition.nextPersonId, assignmentId: createdAssignment.id, userId: actorId, + previousStatus: transition.previousStatus, }, tx, ) diff --git a/tests/integration/use-cases/asset.use-cases.test.ts b/tests/integration/use-cases/asset.use-cases.test.ts index bd3a975..1c038ab 100644 --- a/tests/integration/use-cases/asset.use-cases.test.ts +++ b/tests/integration/use-cases/asset.use-cases.test.ts @@ -446,6 +446,11 @@ describe("asset use-cases", () => { 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, @@ -458,6 +463,8 @@ describe("asset use-cases", () => { }) expect(movements[1].assetLines[0]).toMatchObject({ assetId: created.assetId, + previousStatus: "AVAILABLE", + newStatus: "ASSIGNED", }) expect(movements[2]).toMatchObject({ assignmentId: activeAssignment.id, @@ -471,9 +478,121 @@ describe("asset use-cases", () => { }) 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 })