From 95c52579d186420cc142c198f1087bbb4a0c913e Mon Sep 17 00:00:00 2001 From: Asis Ferrer Date: Thu, 25 Jun 2026 03:11:15 +0200 Subject: [PATCH] refactor(movements): normalize snapshot convention to mutate-then-write --- src/services/movement.service.ts | 10 +-- src/use-cases/asset.use-cases.ts | 70 +++++++++---------- .../use-cases/asset.use-cases.test.ts | 12 ++++ 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/services/movement.service.ts b/src/services/movement.service.ts index e96298d..aff9b4a 100644 --- a/src/services/movement.service.ts +++ b/src/services/movement.service.ts @@ -61,15 +61,15 @@ function toLegacyMovementType(type: InventoryMovementType) { return type } -function getStockSnapshot(currentStock: number, stockDelta: number) { +function getStockSnapshotFromAfter(afterStock: number, stockDelta: number) { if (stockDelta < 0) { return { - previousStock: currentStock + Math.abs(stockDelta), - newStock: currentStock, + previousStock: afterStock + Math.abs(stockDelta), + newStock: afterStock, } } - const previousStock = Math.max(currentStock - stockDelta, 0) + const previousStock = Math.max(afterStock - stockDelta, 0) return { previousStock, @@ -219,7 +219,7 @@ export const MovementService = { create: { itemId, stockDelta, - ...getStockSnapshot(item.stock, stockDelta), + ...getStockSnapshotFromAfter(item.stock, stockDelta), }, } : undefined, diff --git a/src/use-cases/asset.use-cases.ts b/src/use-cases/asset.use-cases.ts index 774142d..f3b2ef9 100644 --- a/src/use-cases/asset.use-cases.ts +++ b/src/use-cases/asset.use-cases.ts @@ -173,6 +173,10 @@ export async function createAssetUseCase( ) : null + if (status === "AVAILABLE") { + await ItemService.updateStock(itemId, 1, tx) + } + await MovementService.create( { itemId, @@ -186,10 +190,6 @@ export async function createAssetUseCase( tx, ) - if (status === "AVAILABLE") { - await ItemService.updateStock(itemId, 1, tx) - } - return { success: true, assetId: newAsset.id, @@ -290,6 +290,37 @@ export async function updateAssetUseCase( tx, ) + const shouldIncrementNextItemStock = + transition.willBeAvailable && + (!transition.wasAvailable || transition.itemChanged) + const shouldDecrementPreviousItemStock = + transition.wasAvailable && + (!transition.willBeAvailable || transition.itemChanged) + + if (shouldIncrementNextItemStock) { + await ItemService.updateStock(transition.nextItemId, 1, tx) + } + + if (shouldDecrementPreviousItemStock) { + if (!transition.previousItemId) { + throw new AssetTransitionError({ + itemId: ["Previous item not found for available asset"], + }) + } + + const stockWasDecremented = await ItemService.decrementStockIfAvailable( + transition.previousItemId, + 1, + tx, + ) + + if (!stockWasDecremented) { + throw new AssetTransitionError({ + stock: ["Item does not have enough stock"], + }) + } + } + let closedActiveAssignment = false if (transition.activeAssignment && !transition.willBeAssigned) { @@ -325,13 +356,6 @@ export async function updateAssetUseCase( closedActiveAssignment = true } - const shouldIncrementNextItemStock = - transition.willBeAvailable && - (!transition.wasAvailable || transition.itemChanged) - const shouldDecrementPreviousItemStock = - transition.wasAvailable && - (!transition.willBeAvailable || transition.itemChanged) - if ( transition.statusChanged && !transition.hasPerson && @@ -422,30 +446,6 @@ export async function updateAssetUseCase( ) } - if (shouldIncrementNextItemStock) { - await ItemService.updateStock(transition.nextItemId, 1, tx) - } - - if (shouldDecrementPreviousItemStock) { - if (!transition.previousItemId) { - throw new AssetTransitionError({ - itemId: ["Previous item not found for available asset"], - }) - } - - const stockWasDecremented = await ItemService.decrementStockIfAvailable( - transition.previousItemId, - 1, - tx, - ) - - if (!stockWasDecremented) { - throw new AssetTransitionError({ - stock: ["Item does not have enough stock"], - }) - } - } - if (transition.willBeAssigned && transition.nextPersonId) { const activeAssignment = transition.activeAssignment diff --git a/tests/integration/use-cases/asset.use-cases.test.ts b/tests/integration/use-cases/asset.use-cases.test.ts index ce6659f..a69cb5c 100644 --- a/tests/integration/use-cases/asset.use-cases.test.ts +++ b/tests/integration/use-cases/asset.use-cases.test.ts @@ -80,6 +80,8 @@ describe("asset use-cases", () => { expect(movements[0].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: 1, + previousStock: 0, + newStock: 1, }) expect(movements[0].assetLines[0]).toMatchObject({ assetId: result.assetId, @@ -343,6 +345,12 @@ describe("asset use-cases", () => { "ASSIGNMENT", "RETURN", ]) + expect(movements[0].stockLines[0]).toMatchObject({ + itemId: item.id, + stockDelta: 1, + previousStock: 0, + newStock: 1, + }) expect(movements[1]).toMatchObject({ assignmentId: activeAssignment.id, performedById: actor.id, @@ -350,6 +358,8 @@ describe("asset use-cases", () => { expect(movements[1].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: -1, + previousStock: 1, + newStock: 0, }) expect(movements[1].assetLines[0]).toMatchObject({ assetId: created.assetId, @@ -361,6 +371,8 @@ describe("asset use-cases", () => { expect(movements[2].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: 1, + previousStock: 0, + newStock: 1, }) expect(movements[2].assetLines[0]).toMatchObject({ assetId: created.assetId,