From b4b63e107a6cdebcdc57696fb873e05518168b26 Mon Sep 17 00:00:00 2001 From: Asis Ferrer Date: Thu, 25 Jun 2026 17:05:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(assets):=20pair=20ASSIGNMENT=20movement=20?= =?UTF-8?q?on=20itemChanged=20AVAILABLE=E2=86=92ASSIGNED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/use-cases/asset.use-cases.ts | 40 +++-- .../use-cases/asset.use-cases.test.ts | 139 ++++++++++++++++++ 2 files changed, 166 insertions(+), 13 deletions(-) diff --git a/src/use-cases/asset.use-cases.ts b/src/use-cases/asset.use-cases.ts index 9995f56..d5a66d7 100644 --- a/src/use-cases/asset.use-cases.ts +++ b/src/use-cases/asset.use-cases.ts @@ -453,6 +453,18 @@ export async function updateAssetUseCase( }, tx, ) + + await MovementService.create( + { + assetId: id, + quantity: 1, + type: "ASSIGNMENT", + details: `Asset assigned from item ${transition.previousItemId} to item ${transition.nextItemId}`, + userId: actorId, + previousStatus: transition.previousStatus, + }, + tx, + ) } if (transition.willBeAssigned && transition.nextPersonId) { @@ -526,19 +538,21 @@ export async function updateAssetUseCase( tx, ) - await MovementService.create( - { - type: "ASSIGNMENT", - quantity: 1, - itemId, - assetId: id, - personId: transition.nextPersonId, - assignmentId: createdAssignment.id, - userId: actorId, - previousStatus: transition.previousStatus, - }, - tx, - ) + if (!(transition.itemChanged && transition.wasAvailable)) { + await MovementService.create( + { + type: "ASSIGNMENT", + quantity: 1, + itemId, + assetId: id, + 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 1c038ab..d422e84 100644 --- a/tests/integration/use-cases/asset.use-cases.test.ts +++ b/tests/integration/use-cases/asset.use-cases.test.ts @@ -693,4 +693,143 @@ describe("asset use-cases", () => { assetId: created.assetId, }) }) + + it("writes a paired OUT and ASSIGNMENT on itemChanged AVAILABLE to ASSIGNED for QUANTITY items", async () => { + const actor = await createTestUser(prisma) + const person = await createTestPerson(prisma) + const sourceItem = await createTestItem(prisma, { stock: 0 }) + const targetItem = await createTestItem(prisma, { stock: 0 }) + + const created = await createAssetUseCase({ + actorId: actor.id, + itemId: sourceItem.id, + serialNumber: "ASSET-W2-QTY-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-W2-QTY-001", + status: "ASSIGNED", + personId: person.id, + }) + expect(updated.success).toBe(true) + if (!updated.success) throw new Error("Expected asset update success") + + const movements = await prisma.inventoryMovement.findMany({ + include: { stockLines: true, assetLines: true }, + orderBy: [{ createdAt: "asc" }, { id: "asc" }], + }) + + // RECEIPT (from create) + OUT (on A) + ASSIGNMENT (on B) = 3 total + expect(movements).toHaveLength(3) + expect(movements.map((movement) => movement.type)).toEqual([ + "RECEIPT", + "ISSUE", + "ASSIGNMENT", + ]) + + const outMovement = movements[1] + const assignmentMovement = movements[2] + + expect(outMovement.stockLines[0]).toMatchObject({ + itemId: sourceItem.id, + stockDelta: -1, + previousStock: 1, + newStock: 0, + }) + expect(outMovement.assetLines).toHaveLength(1) + expect(outMovement.assetLines[0]).toMatchObject({ + assetId: created.assetId, + previousStatus: "AVAILABLE", + newStatus: "ASSIGNED", + }) + + expect(assignmentMovement.stockLines).toEqual([]) + expect(assignmentMovement.assetLines).toHaveLength(1) + expect(assignmentMovement.assetLines[0]).toMatchObject({ + assetId: created.assetId, + previousStatus: "AVAILABLE", + newStatus: "ASSIGNED", + }) + }) + + it("writes a paired OUT and ASSIGNMENT on itemChanged AVAILABLE to ASSIGNED for SERIALIZED items", async () => { + const actor = await createTestUser(prisma) + const person = await createTestPerson(prisma) + const category = await createTestCategory(prisma) + const sourceItem = await prisma.item.create({ + data: { + sku: "W2-SERIAL-A-SKU", + name: "Serial A", + trackingType: "SERIALIZED", + stock: 0, + category: { connect: { id: category.id } }, + }, + }) + const targetItem = await prisma.item.create({ + data: { + sku: "W2-SERIAL-B-SKU", + name: "Serial B", + trackingType: "SERIALIZED", + stock: 0, + category: { connect: { id: category.id } }, + }, + }) + + const created = await createAssetUseCase({ + actorId: actor.id, + itemId: sourceItem.id, + serialNumber: "ASSET-W2-SER-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-W2-SER-001", + status: "ASSIGNED", + personId: person.id, + }) + expect(updated.success).toBe(true) + if (!updated.success) throw new Error("Expected asset update success") + + const movements = await prisma.inventoryMovement.findMany({ + include: { stockLines: true, assetLines: true }, + orderBy: [{ createdAt: "asc" }, { id: "asc" }], + }) + + expect(movements).toHaveLength(3) + expect(movements.map((movement) => movement.type)).toEqual([ + "RECEIPT", + "ISSUE", + "ASSIGNMENT", + ]) + + const outMovement = movements[1] + const assignmentMovement = movements[2] + + expect(outMovement.stockLines).toEqual([]) + expect(outMovement.assetLines).toHaveLength(1) + expect(outMovement.assetLines[0]).toMatchObject({ + assetId: created.assetId, + previousStatus: "AVAILABLE", + newStatus: "ASSIGNED", + }) + + expect(assignmentMovement.stockLines).toEqual([]) + expect(assignmentMovement.assetLines).toHaveLength(1) + expect(assignmentMovement.assetLines[0]).toMatchObject({ + assetId: created.assetId, + previousStatus: "AVAILABLE", + newStatus: "ASSIGNED", + }) + }) })