feat(assignments): support line-based returns and authenticated updates
This commit is contained in:
@@ -2,7 +2,11 @@ import type { Prisma } from "@/generated/prisma/client"
|
||||
import { paginate } from "@/lib/paginate"
|
||||
import prisma from "@/lib/prisma"
|
||||
import type { CreateAssignmentData } from "@/schemas/assignment.schema"
|
||||
import type { Assignment, AssignmentWithPersonItemAsset } from "@/types"
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentStockReturnInput,
|
||||
AssignmentWithPersonItemAsset,
|
||||
} from "@/types"
|
||||
|
||||
type LegacyAssignmentWriteData = CreateAssignmentData & {
|
||||
createdBy?: string
|
||||
@@ -13,6 +17,12 @@ type LegacyAssignmentUpdateData = Partial<LegacyAssignmentWriteData> & {
|
||||
returnDate?: Date | null
|
||||
}
|
||||
|
||||
type ReturnStockAssignmentResult = {
|
||||
returnedQuantity: number
|
||||
fullyReturned: boolean
|
||||
returnedLines: { itemId: string; quantity: number }[]
|
||||
}
|
||||
|
||||
type AssignmentWithLines = Prisma.AssignmentGetPayload<{
|
||||
include: {
|
||||
person: true
|
||||
@@ -27,6 +37,29 @@ const assignmentInclude = {
|
||||
assetLines: { include: { asset: { include: { item: true } } } },
|
||||
} 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(
|
||||
assignment: AssignmentWithLines,
|
||||
): AssignmentWithPersonItemAsset {
|
||||
@@ -51,7 +84,7 @@ function toLegacyAssignment(
|
||||
}
|
||||
|
||||
function toLegacyAssignmentRecord(
|
||||
assignment: Prisma.AssignmentGetPayload<{}>,
|
||||
assignment: Prisma.AssignmentGetPayload<Prisma.AssignmentDefaultArgs>,
|
||||
data: Pick<LegacyAssignmentUpdateData, "itemId" | "assetId" | "quantity">,
|
||||
): Assignment {
|
||||
return {
|
||||
@@ -132,6 +165,120 @@ export const AssignmentService = {
|
||||
|
||||
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 (
|
||||
personId: string,
|
||||
): Promise<AssignmentWithPersonItemAsset[]> => {
|
||||
@@ -233,19 +380,25 @@ export const AssignmentService = {
|
||||
id,
|
||||
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
|
||||
},
|
||||
include: { stockLines: true, assetLines: { include: { asset: true } } },
|
||||
include: {
|
||||
stockLines: true,
|
||||
assetLines: { include: { asset: true } },
|
||||
},
|
||||
})
|
||||
|
||||
if (!assignment) return false
|
||||
|
||||
await Promise.all(
|
||||
assignment.stockLines.map((line) =>
|
||||
db.assignmentStockLine.update({
|
||||
where: { id: line.id },
|
||||
data: { returnedQuantity: line.quantity },
|
||||
}),
|
||||
),
|
||||
)
|
||||
if (assignment.stockLines.length > 0) {
|
||||
const result = await AssignmentService.returnStockAssignment(
|
||||
id,
|
||||
actorId ?? assignment.createdById,
|
||||
undefined,
|
||||
db,
|
||||
)
|
||||
|
||||
return Boolean(result?.fullyReturned)
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
assignment.assetLines
|
||||
.filter((line) => !line.returnedAt)
|
||||
|
||||
Reference in New Issue
Block a user