feat(assignments): emit ADJUSTMENT movement on quantity change
This commit is contained in:
@@ -244,8 +244,17 @@ export async function updateAssignmentUseCase(
|
||||
return updateAssignmentError({ id: ["Assignment not found"] })
|
||||
}
|
||||
|
||||
if (
|
||||
assignment.status === "RETURNED" ||
|
||||
assignment.status === "CANCELLED"
|
||||
) {
|
||||
return updateAssignmentError({ id: ["Assignment is closed"] })
|
||||
}
|
||||
|
||||
let item = null
|
||||
|
||||
if (itemId) {
|
||||
const item = await ItemService.findById(itemId, tx)
|
||||
item = await ItemService.findById(itemId, tx)
|
||||
|
||||
if (!item) {
|
||||
return updateAssignmentError({ itemId: ["Item not found"] })
|
||||
@@ -308,6 +317,22 @@ export async function updateAssignmentUseCase(
|
||||
tx,
|
||||
)
|
||||
} else {
|
||||
if (item && assignment.quantity !== quantity) {
|
||||
const stockDelta = quantity - (assignment.quantity ?? 0)
|
||||
const adjustmentItemId = item.id
|
||||
await ItemService.updateStock(adjustmentItemId, -stockDelta, tx)
|
||||
await MovementService.create(
|
||||
{
|
||||
type: "ADJUSTMENT",
|
||||
itemId: adjustmentItemId,
|
||||
quantity: Math.abs(stockDelta),
|
||||
stockDeltaSign: stockDelta < 0 ? 1 : -1,
|
||||
userId: actorId,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
}
|
||||
|
||||
await AssignmentService.update(
|
||||
id,
|
||||
{
|
||||
|
||||
@@ -408,4 +408,240 @@ describe("assignment use-cases", () => {
|
||||
|
||||
expect(await prisma.inventoryMovement.count()).toBe(2)
|
||||
})
|
||||
|
||||
it("writes an ADJUSTMENT movement with stockDelta=+1 when quantity decreases on an OPEN assignment", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const person = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 5 })
|
||||
|
||||
const created = await createAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
personId: person.id,
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
|
||||
const updated = await updateAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
id: created.assignmentId,
|
||||
personId: person.id,
|
||||
lines: [{ itemId: item.id, quantity: 1 }],
|
||||
})
|
||||
expect(updated.success).toBe(true)
|
||||
if (!updated.success) throw new Error("Expected assignment update success")
|
||||
|
||||
const [updatedItem, movements] = await Promise.all([
|
||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
}),
|
||||
])
|
||||
|
||||
expect(updatedItem.stock).toBe(4)
|
||||
const adjustment = movements.find((movement) => movement.type === "ADJUSTMENT")
|
||||
expect(adjustment).toBeDefined()
|
||||
expect(adjustment).toMatchObject({
|
||||
type: "ADJUSTMENT",
|
||||
reason: "INVENTORY_CORRECTION",
|
||||
})
|
||||
expect(adjustment?.stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
stockDelta: 1,
|
||||
previousStock: 3,
|
||||
newStock: 4,
|
||||
})
|
||||
})
|
||||
|
||||
it("writes an ADJUSTMENT movement with stockDelta=+2 when quantity decreases on a PARTIALLY_RETURNED assignment", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const person = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 5 })
|
||||
|
||||
const created = await createAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
personId: person.id,
|
||||
lines: [{ itemId: item.id, quantity: 3 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
|
||||
const createdAssignment = await prisma.assignment.findUniqueOrThrow({
|
||||
where: { id: created.assignmentId },
|
||||
include: { stockLines: true },
|
||||
})
|
||||
|
||||
const partialReturn = await returnAssignmentUseCase({
|
||||
id: created.assignmentId,
|
||||
actorId: actor.id,
|
||||
returns: [
|
||||
{
|
||||
assignmentLineId: createdAssignment.stockLines[0].id,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(partialReturn).toEqual({ success: true })
|
||||
|
||||
const updated = await updateAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
id: created.assignmentId,
|
||||
personId: person.id,
|
||||
lines: [{ itemId: item.id, quantity: 1 }],
|
||||
})
|
||||
expect(updated.success).toBe(true)
|
||||
if (!updated.success) throw new Error("Expected assignment update success")
|
||||
|
||||
const [updatedItem, movements] = await Promise.all([
|
||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
}),
|
||||
])
|
||||
|
||||
expect(updatedItem.stock).toBe(5)
|
||||
const adjustment = movements.find((movement) => movement.type === "ADJUSTMENT")
|
||||
expect(adjustment).toBeDefined()
|
||||
expect(adjustment).toMatchObject({
|
||||
type: "ADJUSTMENT",
|
||||
reason: "INVENTORY_CORRECTION",
|
||||
})
|
||||
expect(adjustment?.stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
stockDelta: 2,
|
||||
previousStock: 3,
|
||||
newStock: 5,
|
||||
})
|
||||
})
|
||||
|
||||
it("writes an ADJUSTMENT movement with stockDelta=-1 when quantity increases on an OPEN assignment", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const person = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 5 })
|
||||
|
||||
const created = await createAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
personId: person.id,
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
|
||||
const updated = await updateAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
id: created.assignmentId,
|
||||
personId: person.id,
|
||||
lines: [{ itemId: item.id, quantity: 3 }],
|
||||
})
|
||||
expect(updated.success).toBe(true)
|
||||
if (!updated.success) throw new Error("Expected assignment update success")
|
||||
|
||||
const [updatedItem, movements] = await Promise.all([
|
||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
}),
|
||||
])
|
||||
|
||||
expect(updatedItem.stock).toBe(2)
|
||||
const adjustment = movements.find((movement) => movement.type === "ADJUSTMENT")
|
||||
expect(adjustment).toBeDefined()
|
||||
expect(adjustment).toMatchObject({
|
||||
type: "ADJUSTMENT",
|
||||
reason: "INVENTORY_CORRECTION",
|
||||
})
|
||||
expect(adjustment?.stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
stockDelta: -1,
|
||||
previousStock: 3,
|
||||
newStock: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects a quantity change on a RETURNED assignment and writes no movement", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const person = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 5 })
|
||||
|
||||
const created = await createAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
personId: person.id,
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
|
||||
const returned = await returnAssignmentUseCase({
|
||||
id: created.assignmentId,
|
||||
actorId: actor.id,
|
||||
})
|
||||
expect(returned).toEqual({ success: true })
|
||||
|
||||
const movementsBefore = await prisma.inventoryMovement.count()
|
||||
|
||||
const updated = await updateAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
id: created.assignmentId,
|
||||
personId: person.id,
|
||||
lines: [{ itemId: item.id, quantity: 3 }],
|
||||
})
|
||||
|
||||
expect(updated).toEqual({
|
||||
success: false,
|
||||
errors: { id: ["Assignment is closed"] },
|
||||
})
|
||||
|
||||
const [assignment, movements] = await Promise.all([
|
||||
prisma.assignment.findUniqueOrThrow({
|
||||
where: { id: created.assignmentId },
|
||||
include: { stockLines: true },
|
||||
}),
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true },
|
||||
}),
|
||||
])
|
||||
|
||||
expect(assignment.stockLines[0].quantity).toBe(2)
|
||||
expect(movements).toHaveLength(movementsBefore)
|
||||
expect(movements.some((movement) => movement.type === "ADJUSTMENT")).toBe(false)
|
||||
})
|
||||
|
||||
it("writes no movement when quantity is unchanged on an OPEN assignment", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const person = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 5 })
|
||||
|
||||
const created = await createAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
personId: person.id,
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
|
||||
const movementsBefore = await prisma.inventoryMovement.count()
|
||||
|
||||
const updated = await updateAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
id: created.assignmentId,
|
||||
personId: person.id,
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(updated).toEqual({ success: true })
|
||||
|
||||
const [updatedItem, movements] = await Promise.all([
|
||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
}),
|
||||
])
|
||||
|
||||
expect(updatedItem.stock).toBe(3)
|
||||
expect(movements).toHaveLength(movementsBefore)
|
||||
expect(movements.some((movement) => movement.type === "ADJUSTMENT")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user