feat(assignments): support line-based returns and authenticated updates
This commit is contained in:
@@ -37,6 +37,13 @@ export async function createAssignment(formData: CreateAssignmentFormType) {
|
|||||||
|
|
||||||
const result = await createAssignmentUseCase({
|
const result = await createAssignmentUseCase({
|
||||||
...validatedFields.data,
|
...validatedFields.data,
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
itemId: validatedFields.data.itemId!,
|
||||||
|
quantity: validatedFields.data.quantity!,
|
||||||
|
notes: validatedFields.data.notes,
|
||||||
|
},
|
||||||
|
],
|
||||||
actorId: createdBy,
|
actorId: createdBy,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -81,6 +88,13 @@ export async function updateAssignment(formData: UpdateAssignmentFormType) {
|
|||||||
|
|
||||||
const result = await updateAssignmentUseCase({
|
const result = await updateAssignmentUseCase({
|
||||||
...validatedFields.data,
|
...validatedFields.data,
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
itemId: validatedFields.data.itemId!,
|
||||||
|
quantity: validatedFields.data.quantity!,
|
||||||
|
notes: validatedFields.data.notes,
|
||||||
|
},
|
||||||
|
],
|
||||||
actorId: createdBy,
|
actorId: createdBy,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import type { Prisma } from "@/generated/prisma/client"
|
|||||||
import { paginate } from "@/lib/paginate"
|
import { paginate } from "@/lib/paginate"
|
||||||
import prisma from "@/lib/prisma"
|
import prisma from "@/lib/prisma"
|
||||||
import type { CreateAssignmentData } from "@/schemas/assignment.schema"
|
import type { CreateAssignmentData } from "@/schemas/assignment.schema"
|
||||||
import type { Assignment, AssignmentWithPersonItemAsset } from "@/types"
|
import type {
|
||||||
|
Assignment,
|
||||||
|
AssignmentStockReturnInput,
|
||||||
|
AssignmentWithPersonItemAsset,
|
||||||
|
} from "@/types"
|
||||||
|
|
||||||
type LegacyAssignmentWriteData = CreateAssignmentData & {
|
type LegacyAssignmentWriteData = CreateAssignmentData & {
|
||||||
createdBy?: string
|
createdBy?: string
|
||||||
@@ -13,6 +17,12 @@ type LegacyAssignmentUpdateData = Partial<LegacyAssignmentWriteData> & {
|
|||||||
returnDate?: Date | null
|
returnDate?: Date | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReturnStockAssignmentResult = {
|
||||||
|
returnedQuantity: number
|
||||||
|
fullyReturned: boolean
|
||||||
|
returnedLines: { itemId: string; quantity: number }[]
|
||||||
|
}
|
||||||
|
|
||||||
type AssignmentWithLines = Prisma.AssignmentGetPayload<{
|
type AssignmentWithLines = Prisma.AssignmentGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
person: true
|
person: true
|
||||||
@@ -27,6 +37,29 @@ const assignmentInclude = {
|
|||||||
assetLines: { include: { asset: { include: { item: true } } } },
|
assetLines: { include: { asset: { include: { item: true } } } },
|
||||||
} satisfies Prisma.AssignmentInclude
|
} satisfies Prisma.AssignmentInclude
|
||||||
|
|
||||||
|
function normalizeReturnLines(
|
||||||
|
returns: AssignmentStockReturnInput[],
|
||||||
|
): AssignmentStockReturnInput[] {
|
||||||
|
const aggregated = new Map<string, AssignmentStockReturnInput>()
|
||||||
|
|
||||||
|
for (const entry of returns) {
|
||||||
|
const existing = aggregated.get(entry.assignmentLineId)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
aggregated.set(entry.assignmentLineId, {
|
||||||
|
...existing,
|
||||||
|
quantity: existing.quantity + entry.quantity,
|
||||||
|
notes: existing.notes ?? entry.notes,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregated.set(entry.assignmentLineId, { ...entry })
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...aggregated.values()]
|
||||||
|
}
|
||||||
|
|
||||||
function toLegacyAssignment(
|
function toLegacyAssignment(
|
||||||
assignment: AssignmentWithLines,
|
assignment: AssignmentWithLines,
|
||||||
): AssignmentWithPersonItemAsset {
|
): AssignmentWithPersonItemAsset {
|
||||||
@@ -51,7 +84,7 @@ function toLegacyAssignment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toLegacyAssignmentRecord(
|
function toLegacyAssignmentRecord(
|
||||||
assignment: Prisma.AssignmentGetPayload<{}>,
|
assignment: Prisma.AssignmentGetPayload<Prisma.AssignmentDefaultArgs>,
|
||||||
data: Pick<LegacyAssignmentUpdateData, "itemId" | "assetId" | "quantity">,
|
data: Pick<LegacyAssignmentUpdateData, "itemId" | "assetId" | "quantity">,
|
||||||
): Assignment {
|
): Assignment {
|
||||||
return {
|
return {
|
||||||
@@ -132,6 +165,120 @@ export const AssignmentService = {
|
|||||||
|
|
||||||
return assignment ? toLegacyAssignment(assignment) : null
|
return assignment ? toLegacyAssignment(assignment) : null
|
||||||
},
|
},
|
||||||
|
returnStockAssignment: async (
|
||||||
|
id: string,
|
||||||
|
receivedById: string | undefined,
|
||||||
|
returns: AssignmentStockReturnInput[] | undefined,
|
||||||
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
|
): Promise<ReturnStockAssignmentResult | null> => {
|
||||||
|
const assignment = await db.assignment.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
stockLines: {
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "asc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!assignment || assignment.stockLines.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnerId = receivedById ?? assignment.createdById
|
||||||
|
|
||||||
|
const remainingReturns =
|
||||||
|
returns !== undefined
|
||||||
|
? normalizeReturnLines(returns)
|
||||||
|
: assignment.stockLines
|
||||||
|
.filter((line) => line.returnedQuantity < line.quantity)
|
||||||
|
.map((line) => ({
|
||||||
|
assignmentLineId: line.id,
|
||||||
|
quantity: line.quantity - line.returnedQuantity,
|
||||||
|
notes: undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (remainingReturns.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const stockLinesById = new Map(
|
||||||
|
assignment.stockLines.map((line) => [line.id, line]),
|
||||||
|
)
|
||||||
|
const returnTimestamp = new Date()
|
||||||
|
let returnedQuantity = 0
|
||||||
|
const returnedLines: { itemId: string; quantity: number }[] = []
|
||||||
|
|
||||||
|
for (const returnLine of remainingReturns) {
|
||||||
|
const stockLine = stockLinesById.get(returnLine.assignmentLineId)
|
||||||
|
|
||||||
|
if (!stockLine) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(returnLine.quantity) || returnLine.quantity <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingQuantity = stockLine.quantity - stockLine.returnedQuantity
|
||||||
|
|
||||||
|
if (returnLine.quantity > remainingQuantity) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.assignmentStockReturn.create({
|
||||||
|
data: {
|
||||||
|
assignmentLineId: stockLine.id,
|
||||||
|
quantity: returnLine.quantity,
|
||||||
|
receivedById: returnerId,
|
||||||
|
returnedAt: returnTimestamp,
|
||||||
|
notes: returnLine.notes,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await db.assignmentStockLine.update({
|
||||||
|
where: { id: stockLine.id },
|
||||||
|
data: {
|
||||||
|
returnedQuantity: {
|
||||||
|
increment: returnLine.quantity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
returnedQuantity += returnLine.quantity
|
||||||
|
returnedLines.push({
|
||||||
|
itemId: stockLine.itemId,
|
||||||
|
quantity: returnLine.quantity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullyReturned = assignment.stockLines.every(
|
||||||
|
(line) =>
|
||||||
|
line.returnedQuantity +
|
||||||
|
(remainingReturns.find((entry) => entry.assignmentLineId === line.id)
|
||||||
|
?.quantity ?? 0) >=
|
||||||
|
line.quantity,
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.assignment.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
status: fullyReturned ? "RETURNED" : "PARTIALLY_RETURNED",
|
||||||
|
closedAt: fullyReturned ? returnTimestamp : null,
|
||||||
|
closedById: fullyReturned ? returnerId : null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
returnedQuantity,
|
||||||
|
fullyReturned,
|
||||||
|
returnedLines,
|
||||||
|
}
|
||||||
|
},
|
||||||
findAllByPerson: async (
|
findAllByPerson: async (
|
||||||
personId: string,
|
personId: string,
|
||||||
): Promise<AssignmentWithPersonItemAsset[]> => {
|
): Promise<AssignmentWithPersonItemAsset[]> => {
|
||||||
@@ -233,19 +380,25 @@ export const AssignmentService = {
|
|||||||
id,
|
id,
|
||||||
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
|
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
|
||||||
},
|
},
|
||||||
include: { stockLines: true, assetLines: { include: { asset: true } } },
|
include: {
|
||||||
|
stockLines: true,
|
||||||
|
assetLines: { include: { asset: true } },
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!assignment) return false
|
if (!assignment) return false
|
||||||
|
|
||||||
await Promise.all(
|
if (assignment.stockLines.length > 0) {
|
||||||
assignment.stockLines.map((line) =>
|
const result = await AssignmentService.returnStockAssignment(
|
||||||
db.assignmentStockLine.update({
|
id,
|
||||||
where: { id: line.id },
|
actorId ?? assignment.createdById,
|
||||||
data: { returnedQuantity: line.quantity },
|
undefined,
|
||||||
}),
|
db,
|
||||||
),
|
)
|
||||||
)
|
|
||||||
|
return Boolean(result?.fullyReturned)
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
assignment.assetLines
|
assignment.assetLines
|
||||||
.filter((line) => !line.returnedAt)
|
.filter((line) => !line.returnedAt)
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ export type Assignment = PrismaAssignment & {
|
|||||||
|
|
||||||
export type AssignmentSummary = Pick<Assignment, "id" | "quantity">
|
export type AssignmentSummary = Pick<Assignment, "id" | "quantity">
|
||||||
|
|
||||||
|
export type AssignmentStockLineInput = {
|
||||||
|
itemId: string
|
||||||
|
quantity: number
|
||||||
|
notes?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AssignmentStockReturnInput = {
|
||||||
|
assignmentLineId: string
|
||||||
|
quantity: number
|
||||||
|
notes?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AssignmentWithPersonItemAsset = Assignment & {
|
export type AssignmentWithPersonItemAsset = Assignment & {
|
||||||
assignmentDate: Date
|
assignmentDate: Date
|
||||||
returnDate: Date | null
|
returnDate: Date | null
|
||||||
|
|||||||
@@ -7,20 +7,37 @@ import { AssetService } from "@/services/asset.service"
|
|||||||
import { AssignmentService } from "@/services/assignment.service"
|
import { AssignmentService } from "@/services/assignment.service"
|
||||||
import { ItemService } from "@/services/item.service"
|
import { ItemService } from "@/services/item.service"
|
||||||
import { MovementService } from "@/services/movement.service"
|
import { MovementService } from "@/services/movement.service"
|
||||||
|
import type {
|
||||||
|
AssignmentStockLineInput,
|
||||||
|
AssignmentStockReturnInput,
|
||||||
|
} from "@/types"
|
||||||
|
|
||||||
type FieldErrors = Record<string, string[]>
|
type FieldErrors = Record<string, string[]>
|
||||||
|
|
||||||
type CreateAssignmentUseCaseInput = CreateAssignmentData & {
|
type CreateAssignmentUseCaseInput = Omit<
|
||||||
|
CreateAssignmentData,
|
||||||
|
"itemId" | "quantity"
|
||||||
|
> & {
|
||||||
actorId: string
|
actorId: string
|
||||||
|
itemId?: string
|
||||||
|
quantity?: number
|
||||||
|
lines?: AssignmentStockLineInput[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReturnAssignmentUseCaseInput = {
|
type ReturnAssignmentUseCaseInput = {
|
||||||
id: string
|
id: string
|
||||||
actorId: string
|
actorId: string
|
||||||
|
returns?: AssignmentStockReturnInput[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateAssignmentUseCaseInput = UpdateAssignmentData & {
|
type UpdateAssignmentUseCaseInput = Omit<
|
||||||
|
UpdateAssignmentData,
|
||||||
|
"itemId" | "quantity"
|
||||||
|
> & {
|
||||||
actorId: string
|
actorId: string
|
||||||
|
itemId?: string
|
||||||
|
quantity?: number
|
||||||
|
lines?: AssignmentStockLineInput[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateAssignmentUseCaseResult =
|
type CreateAssignmentUseCaseResult =
|
||||||
@@ -78,10 +95,45 @@ function updateAssignmentError(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAssignmentLine(
|
||||||
|
input:
|
||||||
|
| Pick<
|
||||||
|
CreateAssignmentUseCaseInput,
|
||||||
|
"itemId" | "quantity" | "notes" | "lines"
|
||||||
|
>
|
||||||
|
| Pick<
|
||||||
|
UpdateAssignmentUseCaseInput,
|
||||||
|
"itemId" | "quantity" | "notes" | "lines"
|
||||||
|
>,
|
||||||
|
): AssignmentStockLineInput | null {
|
||||||
|
const providedLine = input.lines?.[0]
|
||||||
|
|
||||||
|
if (providedLine) {
|
||||||
|
return providedLine
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.itemId || typeof input.quantity !== "number") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemId: input.itemId,
|
||||||
|
quantity: input.quantity,
|
||||||
|
notes: input.notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function createAssignmentUseCase(
|
export async function createAssignmentUseCase(
|
||||||
input: CreateAssignmentUseCaseInput,
|
input: CreateAssignmentUseCaseInput,
|
||||||
): Promise<CreateAssignmentUseCaseResult> {
|
): Promise<CreateAssignmentUseCaseResult> {
|
||||||
const { actorId, itemId, assetId, quantity, personId } = input
|
const { actorId, assetId, personId } = input
|
||||||
|
const line = resolveAssignmentLine(input)
|
||||||
|
|
||||||
|
if (!line || !personId || line.quantity <= 0) {
|
||||||
|
return createAssignmentError({ error: ["Invalid assignment data"] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { itemId, quantity, notes } = line
|
||||||
|
|
||||||
if (!itemId || !personId || quantity <= 0) {
|
if (!itemId || !personId || quantity <= 0) {
|
||||||
return createAssignmentError({ error: ["Invalid assignment data"] })
|
return createAssignmentError({ error: ["Invalid assignment data"] })
|
||||||
@@ -142,7 +194,7 @@ export async function createAssignmentUseCase(
|
|||||||
assetId: assetId || undefined,
|
assetId: assetId || undefined,
|
||||||
quantity,
|
quantity,
|
||||||
personId,
|
personId,
|
||||||
notes: input.notes,
|
notes: notes ?? input.notes,
|
||||||
assignmentDate: input.assignmentDate,
|
assignmentDate: input.assignmentDate,
|
||||||
createdBy: actorId,
|
createdBy: actorId,
|
||||||
},
|
},
|
||||||
@@ -172,16 +224,12 @@ export async function createAssignmentUseCase(
|
|||||||
export async function updateAssignmentUseCase(
|
export async function updateAssignmentUseCase(
|
||||||
input: UpdateAssignmentUseCaseInput,
|
input: UpdateAssignmentUseCaseInput,
|
||||||
): Promise<UpdateAssignmentUseCaseResult> {
|
): Promise<UpdateAssignmentUseCaseResult> {
|
||||||
const {
|
const { actorId, id, personId, assetId } = input
|
||||||
actorId,
|
const line = resolveAssignmentLine(input)
|
||||||
id,
|
const itemId = line?.itemId ?? input.itemId
|
||||||
personId,
|
const quantity = line?.quantity ?? input.quantity ?? 0
|
||||||
itemId,
|
const notes = line?.notes ?? input.notes
|
||||||
assetId,
|
const assignmentDate = input.assignmentDate
|
||||||
quantity,
|
|
||||||
notes,
|
|
||||||
assignmentDate,
|
|
||||||
} = input
|
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return updateAssignmentError({ id: ["Assignment ID is required"] })
|
return updateAssignmentError({ id: ["Assignment ID is required"] })
|
||||||
@@ -282,7 +330,7 @@ export async function updateAssignmentUseCase(
|
|||||||
export async function returnAssignmentUseCase(
|
export async function returnAssignmentUseCase(
|
||||||
input: ReturnAssignmentUseCaseInput,
|
input: ReturnAssignmentUseCaseInput,
|
||||||
): Promise<ReturnAssignmentUseCaseResult> {
|
): Promise<ReturnAssignmentUseCaseResult> {
|
||||||
const { id, actorId } = input
|
const { id, actorId, returns } = input
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return returnAssignmentError({ id: ["Assignment ID is required"] })
|
return returnAssignmentError({ id: ["Assignment ID is required"] })
|
||||||
@@ -299,18 +347,35 @@ export async function returnAssignmentUseCase(
|
|||||||
return returnAssignmentError({ id: ["Assignment already returned"] })
|
return returnAssignmentError({ id: ["Assignment already returned"] })
|
||||||
}
|
}
|
||||||
|
|
||||||
const assignmentWasReturned = await AssignmentService.markReturnedIfActive(
|
const returnResult = await AssignmentService.returnStockAssignment(
|
||||||
id,
|
id,
|
||||||
actorId,
|
actorId,
|
||||||
|
returns,
|
||||||
tx,
|
tx,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!assignmentWasReturned) {
|
if (!returnResult) {
|
||||||
return returnAssignmentError({ id: ["Assignment already returned"] })
|
return returnAssignmentError({ error: ["Invalid assignment data"] })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assignment.itemId && assignment.quantity) {
|
for (const returnedLine of returnResult.returnedLines) {
|
||||||
await ItemService.updateStock(assignment.itemId, assignment.quantity, tx)
|
await ItemService.updateStock(
|
||||||
|
returnedLine.itemId,
|
||||||
|
returnedLine.quantity,
|
||||||
|
tx,
|
||||||
|
)
|
||||||
|
|
||||||
|
await MovementService.create(
|
||||||
|
{
|
||||||
|
type: "RETURN",
|
||||||
|
quantity: returnedLine.quantity,
|
||||||
|
itemId: returnedLine.itemId,
|
||||||
|
assetId: assignment.assetId || undefined,
|
||||||
|
assignmentId: id,
|
||||||
|
userId: actorId,
|
||||||
|
},
|
||||||
|
tx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assignment.assetId) {
|
if (assignment.assetId) {
|
||||||
@@ -323,18 +388,6 @@ export async function returnAssignmentUseCase(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await MovementService.create(
|
|
||||||
{
|
|
||||||
type: "RETURN",
|
|
||||||
quantity: assignment.quantity || 1,
|
|
||||||
itemId: assignment.itemId || undefined,
|
|
||||||
assetId: assignment.assetId || undefined,
|
|
||||||
assignmentId: id,
|
|
||||||
userId: actorId,
|
|
||||||
},
|
|
||||||
tx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
|
|
||||||
let prisma: PrismaClient
|
let prisma: PrismaClient
|
||||||
let createAssignmentUseCase: typeof import("@/use-cases/assignment.use-cases").createAssignmentUseCase
|
let createAssignmentUseCase: typeof import("@/use-cases/assignment.use-cases").createAssignmentUseCase
|
||||||
|
let updateAssignmentUseCase: typeof import("@/use-cases/assignment.use-cases").updateAssignmentUseCase
|
||||||
let returnAssignmentUseCase: typeof import("@/use-cases/assignment.use-cases").returnAssignmentUseCase
|
let returnAssignmentUseCase: typeof import("@/use-cases/assignment.use-cases").returnAssignmentUseCase
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -23,6 +24,7 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
prisma = prismaModule.prisma
|
prisma = prismaModule.prisma
|
||||||
createAssignmentUseCase = assignmentUseCases.createAssignmentUseCase
|
createAssignmentUseCase = assignmentUseCases.createAssignmentUseCase
|
||||||
|
updateAssignmentUseCase = assignmentUseCases.updateAssignmentUseCase
|
||||||
returnAssignmentUseCase = assignmentUseCases.returnAssignmentUseCase
|
returnAssignmentUseCase = assignmentUseCases.returnAssignmentUseCase
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -45,11 +47,10 @@ describe("assignment use-cases", () => {
|
|||||||
|
|
||||||
const result = await createAssignmentUseCase({
|
const result = await createAssignmentUseCase({
|
||||||
actorId: actor.id,
|
actorId: actor.id,
|
||||||
itemId: item.id,
|
|
||||||
personId: person.id,
|
personId: person.id,
|
||||||
quantity: 2,
|
|
||||||
assignmentDate,
|
assignmentDate,
|
||||||
notes: "Initial assignment",
|
notes: "Initial assignment",
|
||||||
|
lines: [{ itemId: item.id, quantity: 2 }],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
@@ -89,6 +90,44 @@ describe("assignment use-cases", () => {
|
|||||||
expect(movements[0].stockLines[0]).toMatchObject({
|
expect(movements[0].stockLines[0]).toMatchObject({
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
stockDelta: -2,
|
stockDelta: -2,
|
||||||
|
previousStock: 5,
|
||||||
|
newStock: 3,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("updates an assignment from a quantity line DTO and keeps the edited line quantity", 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)
|
||||||
|
|
||||||
|
const assignment = await prisma.assignment.findUniqueOrThrow({
|
||||||
|
where: { id: created.assignmentId },
|
||||||
|
include: { stockLines: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(assignment.stockLines).toHaveLength(1)
|
||||||
|
expect(assignment.stockLines[0]).toMatchObject({
|
||||||
|
itemId: item.id,
|
||||||
|
quantity: 1,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -111,11 +150,13 @@ describe("assignment use-cases", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
expect(
|
||||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
await prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||||
).resolves.toMatchObject({ stock: 1 })
|
).toMatchObject({
|
||||||
await expect(prisma.assignment.count()).resolves.toBe(0)
|
stock: 1,
|
||||||
await expect(prisma.inventoryMovement.count()).resolves.toBe(0)
|
})
|
||||||
|
expect(await prisma.assignment.count()).toBe(0)
|
||||||
|
expect(await prisma.inventoryMovement.count()).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns an assignment, restores stock, closes it, and records a RETURN movement", async () => {
|
it("returns an assignment, restores stock, closes it, and records a RETURN movement", async () => {
|
||||||
@@ -145,7 +186,7 @@ describe("assignment use-cases", () => {
|
|||||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||||
prisma.assignment.findUniqueOrThrow({
|
prisma.assignment.findUniqueOrThrow({
|
||||||
where: { id: created.assignmentId },
|
where: { id: created.assignmentId },
|
||||||
include: { stockLines: true },
|
include: { stockLines: { include: { returns: true } } },
|
||||||
}),
|
}),
|
||||||
prisma.inventoryMovement.findMany({
|
prisma.inventoryMovement.findMany({
|
||||||
include: { stockLines: true },
|
include: { stockLines: true },
|
||||||
@@ -158,12 +199,18 @@ describe("assignment use-cases", () => {
|
|||||||
expect(assignment).toMatchObject({
|
expect(assignment).toMatchObject({
|
||||||
personId: person.id,
|
personId: person.id,
|
||||||
status: "RETURNED",
|
status: "RETURNED",
|
||||||
|
closedById: actor.id,
|
||||||
})
|
})
|
||||||
expect(assignment.stockLines[0]).toMatchObject({
|
expect(assignment.stockLines[0]).toMatchObject({
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
quantity: 3,
|
quantity: 3,
|
||||||
returnedQuantity: 3,
|
returnedQuantity: 3,
|
||||||
})
|
})
|
||||||
|
expect(assignment.stockLines[0].returns).toHaveLength(1)
|
||||||
|
expect(assignment.stockLines[0].returns[0]).toMatchObject({
|
||||||
|
quantity: 3,
|
||||||
|
receivedById: actor.id,
|
||||||
|
})
|
||||||
expect(movements).toHaveLength(2)
|
expect(movements).toHaveLength(2)
|
||||||
expect(movements[0]).toMatchObject({
|
expect(movements[0]).toMatchObject({
|
||||||
type: "ASSIGNMENT",
|
type: "ASSIGNMENT",
|
||||||
@@ -173,6 +220,8 @@ describe("assignment use-cases", () => {
|
|||||||
expect(movements[0].stockLines[0]).toMatchObject({
|
expect(movements[0].stockLines[0]).toMatchObject({
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
stockDelta: -3,
|
stockDelta: -3,
|
||||||
|
previousStock: 4,
|
||||||
|
newStock: 1,
|
||||||
})
|
})
|
||||||
expect(movements[1]).toMatchObject({
|
expect(movements[1]).toMatchObject({
|
||||||
type: "RETURN",
|
type: "RETURN",
|
||||||
@@ -182,6 +231,100 @@ describe("assignment use-cases", () => {
|
|||||||
expect(movements[1].stockLines[0]).toMatchObject({
|
expect(movements[1].stockLines[0]).toMatchObject({
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
stockDelta: 3,
|
stockDelta: 3,
|
||||||
|
previousStock: 1,
|
||||||
|
newStock: 4,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("records partial quantity returns before fully closing the 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,
|
||||||
|
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 firstReturn = await returnAssignmentUseCase({
|
||||||
|
id: created.assignmentId,
|
||||||
|
actorId: actor.id,
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
assignmentLineId: assignment.stockLines[0].id,
|
||||||
|
quantity: 2,
|
||||||
|
notes: "First return batch",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(firstReturn).toEqual({ success: true })
|
||||||
|
|
||||||
|
const partiallyReturned = await prisma.assignment.findUniqueOrThrow({
|
||||||
|
where: { id: created.assignmentId },
|
||||||
|
include: { stockLines: { include: { returns: true } } },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(partiallyReturned).toMatchObject({
|
||||||
|
status: "PARTIALLY_RETURNED",
|
||||||
|
closedAt: null,
|
||||||
|
closedById: null,
|
||||||
|
})
|
||||||
|
expect(partiallyReturned.stockLines[0]).toMatchObject({
|
||||||
|
quantity: 5,
|
||||||
|
returnedQuantity: 2,
|
||||||
|
})
|
||||||
|
expect(partiallyReturned.stockLines[0].returns).toHaveLength(1)
|
||||||
|
expect(partiallyReturned.stockLines[0].returns[0]).toMatchObject({
|
||||||
|
quantity: 2,
|
||||||
|
receivedById: actor.id,
|
||||||
|
notes: "First return batch",
|
||||||
|
})
|
||||||
|
|
||||||
|
const secondReturn = await returnAssignmentUseCase({
|
||||||
|
id: created.assignmentId,
|
||||||
|
actorId: actor.id,
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
assignmentLineId: assignment.stockLines[0].id,
|
||||||
|
quantity: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(secondReturn).toEqual({ success: true })
|
||||||
|
|
||||||
|
const fullyReturned = await prisma.assignment.findUniqueOrThrow({
|
||||||
|
where: { id: created.assignmentId },
|
||||||
|
include: { stockLines: { include: { returns: true } } },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(fullyReturned).toMatchObject({
|
||||||
|
status: "RETURNED",
|
||||||
|
closedById: actor.id,
|
||||||
|
})
|
||||||
|
expect(fullyReturned.closedAt).toBeInstanceOf(Date)
|
||||||
|
expect(fullyReturned.stockLines[0]).toMatchObject({
|
||||||
|
quantity: 5,
|
||||||
|
returnedQuantity: 5,
|
||||||
|
})
|
||||||
|
expect(fullyReturned.stockLines[0].returns).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||||
|
).toMatchObject({
|
||||||
|
stock: 5,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -212,6 +355,6 @@ describe("assignment use-cases", () => {
|
|||||||
errors: { id: ["Assignment already returned"] },
|
errors: { id: ["Assignment already returned"] },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(prisma.inventoryMovement.count()).resolves.toBe(2)
|
expect(await prisma.inventoryMovement.count()).toBe(2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user