feat(assignments): partial return schema, concurrency guard, domain error
This commit is contained in:
@@ -287,7 +287,7 @@ describe("assignment use-cases", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("records partial quantity returns before fully closing the assignment", async () => {
|
||||
it("records sequential partial returns of 2, 1, then 2 before fully closing the assignment", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const person = await createTestPerson(prisma)
|
||||
const item = await createTestItem(prisma, { stock: 5 })
|
||||
@@ -349,13 +349,26 @@ describe("assignment use-cases", () => {
|
||||
returns: [
|
||||
{
|
||||
assignmentLineId: assignment.stockLines[0].id,
|
||||
quantity: 3,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(secondReturn).toEqual({ success: true })
|
||||
|
||||
const thirdReturn = await returnAssignmentUseCase({
|
||||
id: created.assignmentId,
|
||||
actorId: actor.id,
|
||||
returns: [
|
||||
{
|
||||
assignmentLineId: assignment.stockLines[0].id,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(thirdReturn).toEqual({ success: true })
|
||||
|
||||
const fullyReturned = await prisma.assignment.findUniqueOrThrow({
|
||||
where: { id: created.assignmentId },
|
||||
include: { stockLines: { include: { returns: true } } },
|
||||
@@ -370,13 +383,102 @@ describe("assignment use-cases", () => {
|
||||
quantity: 5,
|
||||
returnedQuantity: 5,
|
||||
})
|
||||
expect(fullyReturned.stockLines[0].returns).toHaveLength(2)
|
||||
expect(fullyReturned.stockLines[0].returns).toHaveLength(3)
|
||||
|
||||
expect(
|
||||
await prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
).toMatchObject({
|
||||
stock: 5,
|
||||
})
|
||||
|
||||
const returnMovements = await prisma.inventoryMovement.findMany({
|
||||
where: { type: "RETURN" },
|
||||
include: { stockLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
})
|
||||
|
||||
expect(returnMovements).toHaveLength(3)
|
||||
expect(returnMovements[0].stockLines[0]).toMatchObject({
|
||||
stockDelta: 2,
|
||||
previousStock: 0,
|
||||
newStock: 2,
|
||||
})
|
||||
expect(returnMovements[1].stockLines[0]).toMatchObject({
|
||||
stockDelta: 1,
|
||||
previousStock: 2,
|
||||
newStock: 3,
|
||||
})
|
||||
expect(returnMovements[2].stockLines[0]).toMatchObject({
|
||||
stockDelta: 2,
|
||||
previousStock: 3,
|
||||
newStock: 5,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects a concurrent partial return with a domain error", 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,
|
||||
itemId: item.id,
|
||||
personId: person.id,
|
||||
quantity: 5,
|
||||
})
|
||||
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success)
|
||||
throw new Error("Expected assignment creation success")
|
||||
|
||||
const assignment = await prisma.assignment.findUniqueOrThrow({
|
||||
where: { id: created.assignmentId },
|
||||
include: { stockLines: true },
|
||||
})
|
||||
|
||||
const [resultA, resultB] = await Promise.all([
|
||||
returnAssignmentUseCase({
|
||||
id: created.assignmentId,
|
||||
actorId: actor.id,
|
||||
returns: [
|
||||
{
|
||||
assignmentLineId: assignment.stockLines[0].id,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
}),
|
||||
returnAssignmentUseCase({
|
||||
id: created.assignmentId,
|
||||
actorId: actor.id,
|
||||
returns: [
|
||||
{
|
||||
assignmentLineId: assignment.stockLines[0].id,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
|
||||
const successes = [resultA, resultB].filter((result) => result.success)
|
||||
const failures = [resultA, resultB].filter((result) => !result.success)
|
||||
|
||||
expect(successes).toHaveLength(1)
|
||||
expect(failures).toHaveLength(1)
|
||||
expect(failures[0]).toEqual({
|
||||
success: false,
|
||||
errors: { error: ["errorConcurrent"] },
|
||||
})
|
||||
|
||||
const finalAssignment = await prisma.assignment.findUniqueOrThrow({
|
||||
where: { id: created.assignmentId },
|
||||
include: { stockLines: { include: { returns: true } } },
|
||||
})
|
||||
|
||||
expect(finalAssignment.status).toBe("PARTIALLY_RETURNED")
|
||||
expect(finalAssignment.stockLines[0].returns).toHaveLength(1)
|
||||
expect(finalAssignment.stockLines[0].returnedQuantity).toBe(
|
||||
finalAssignment.stockLines[0].returns[0].quantity,
|
||||
)
|
||||
})
|
||||
|
||||
it("rejects returning the same assignment twice", async () => {
|
||||
@@ -420,7 +522,8 @@ describe("assignment use-cases", () => {
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
if (!created.success)
|
||||
throw new Error("Expected assignment creation success")
|
||||
|
||||
const updated = await updateAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
@@ -440,7 +543,9 @@ describe("assignment use-cases", () => {
|
||||
])
|
||||
|
||||
expect(updatedItem.stock).toBe(4)
|
||||
const adjustment = movements.find((movement) => movement.type === "ADJUSTMENT")
|
||||
const adjustment = movements.find(
|
||||
(movement) => movement.type === "ADJUSTMENT",
|
||||
)
|
||||
expect(adjustment).toBeDefined()
|
||||
expect(adjustment).toMatchObject({
|
||||
type: "ADJUSTMENT",
|
||||
@@ -465,7 +570,8 @@ describe("assignment use-cases", () => {
|
||||
lines: [{ itemId: item.id, quantity: 3 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
if (!created.success)
|
||||
throw new Error("Expected assignment creation success")
|
||||
|
||||
const createdAssignment = await prisma.assignment.findUniqueOrThrow({
|
||||
where: { id: created.assignmentId },
|
||||
@@ -502,7 +608,9 @@ describe("assignment use-cases", () => {
|
||||
])
|
||||
|
||||
expect(updatedItem.stock).toBe(5)
|
||||
const adjustment = movements.find((movement) => movement.type === "ADJUSTMENT")
|
||||
const adjustment = movements.find(
|
||||
(movement) => movement.type === "ADJUSTMENT",
|
||||
)
|
||||
expect(adjustment).toBeDefined()
|
||||
expect(adjustment).toMatchObject({
|
||||
type: "ADJUSTMENT",
|
||||
@@ -527,7 +635,8 @@ describe("assignment use-cases", () => {
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
if (!created.success)
|
||||
throw new Error("Expected assignment creation success")
|
||||
|
||||
const updated = await updateAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
@@ -547,7 +656,9 @@ describe("assignment use-cases", () => {
|
||||
])
|
||||
|
||||
expect(updatedItem.stock).toBe(2)
|
||||
const adjustment = movements.find((movement) => movement.type === "ADJUSTMENT")
|
||||
const adjustment = movements.find(
|
||||
(movement) => movement.type === "ADJUSTMENT",
|
||||
)
|
||||
expect(adjustment).toBeDefined()
|
||||
expect(adjustment).toMatchObject({
|
||||
type: "ADJUSTMENT",
|
||||
@@ -572,7 +683,8 @@ describe("assignment use-cases", () => {
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
if (!created.success)
|
||||
throw new Error("Expected assignment creation success")
|
||||
|
||||
const returned = await returnAssignmentUseCase({
|
||||
id: created.assignmentId,
|
||||
@@ -606,7 +718,9 @@ describe("assignment use-cases", () => {
|
||||
|
||||
expect(assignment.stockLines[0].quantity).toBe(2)
|
||||
expect(movements).toHaveLength(movementsBefore)
|
||||
expect(movements.some((movement) => movement.type === "ADJUSTMENT")).toBe(false)
|
||||
expect(movements.some((movement) => movement.type === "ADJUSTMENT")).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
it("writes no movement when quantity is unchanged on an OPEN assignment", async () => {
|
||||
@@ -620,7 +734,8 @@ describe("assignment use-cases", () => {
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
if (!created.success)
|
||||
throw new Error("Expected assignment creation success")
|
||||
|
||||
const movementsBefore = await prisma.inventoryMovement.count()
|
||||
|
||||
@@ -642,7 +757,9 @@ describe("assignment use-cases", () => {
|
||||
|
||||
expect(updatedItem.stock).toBe(3)
|
||||
expect(movements).toHaveLength(movementsBefore)
|
||||
expect(movements.some((movement) => movement.type === "ADJUSTMENT")).toBe(false)
|
||||
expect(movements.some((movement) => movement.type === "ADJUSTMENT")).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
it("closes the old assignment and creates a new one with RETURN+ASSIGNMENT pair on person swap for QUANTITY", async () => {
|
||||
@@ -657,7 +774,8 @@ describe("assignment use-cases", () => {
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
if (!created.success)
|
||||
throw new Error("Expected assignment creation success")
|
||||
|
||||
const updated = await updateAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
@@ -774,7 +892,8 @@ describe("assignment use-cases", () => {
|
||||
lines: [{ itemId: item.id, quantity: 1 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
if (!created.success)
|
||||
throw new Error("Expected assignment creation success")
|
||||
|
||||
const updated = await updateAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
@@ -863,7 +982,8 @@ describe("assignment use-cases", () => {
|
||||
lines: [{ itemId: item.id, quantity: 2 }],
|
||||
})
|
||||
expect(created.success).toBe(true)
|
||||
if (!created.success) throw new Error("Expected assignment creation success")
|
||||
if (!created.success)
|
||||
throw new Error("Expected assignment creation success")
|
||||
|
||||
const updated = await updateAssignmentUseCase({
|
||||
actorId: actor.id,
|
||||
|
||||
Reference in New Issue
Block a user