Files
stock-manager/src/services/assignment.service.ts
T

476 lines
12 KiB
TypeScript

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,
AssignmentStockReturnInput,
AssignmentWithPersonItemAsset,
} from "@/types"
type LegacyAssignmentWriteData = CreateAssignmentData & {
createdBy?: string
createdById?: string
}
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
stockLines: { include: { item: true } }
assetLines: { include: { asset: { include: { item: true } } } }
}
}>
const assignmentInclude = {
person: true,
stockLines: { include: { item: true } },
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 {
const stockLine = assignment.stockLines[0]
const assetLine = assignment.assetLines[0]
const item = stockLine?.item ?? assetLine?.asset.item ?? null
const asset = assetLine?.asset ?? null
const quantity = stockLine?.quantity ?? (assetLine ? 1 : null)
const returnDate = assignment.closedAt ?? assetLine?.returnedAt ?? null
return {
...assignment,
assignmentDate: assignment.assignedAt,
returnDate,
person: assignment.person,
item,
asset,
itemId: item?.id ?? null,
assetId: asset?.id ?? null,
quantity,
}
}
function toLegacyAssignmentRecord(
assignment: Prisma.AssignmentGetPayload<Prisma.AssignmentDefaultArgs>,
data: Pick<LegacyAssignmentUpdateData, "itemId" | "assetId" | "quantity">,
): Assignment {
return {
...assignment,
assignmentDate: assignment.assignedAt,
returnDate: assignment.closedAt,
itemId: data.itemId ?? null,
assetId: data.assetId ?? null,
quantity: data.quantity ?? null,
}
}
export const AssignmentService = {
findAllWithPerson: async (): Promise<AssignmentWithPersonItemAsset[]> => {
const assignments = await prisma.assignment.findMany({
where: {
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
},
include: assignmentInclude,
orderBy: {
createdAt: "desc",
},
})
return assignments.map(toLegacyAssignment)
},
findAllWithPersonPaginated: async ({
page,
pageSize,
search,
}: {
page?: number
pageSize?: number
search?: string
}) => {
const result = await paginate<AssignmentWithLines>({
model: prisma.assignment,
page,
pageSize,
where: {
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
...(search
? {
OR: [
{
person: {
firstName: { contains: search, mode: "insensitive" },
},
},
{
person: {
lastName: { contains: search, mode: "insensitive" },
},
},
],
}
: {}),
},
include: assignmentInclude,
orderBy: {
createdAt: "desc",
},
})
return {
...result,
data: result.data.map(toLegacyAssignment),
}
},
findById: async (
id: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<AssignmentWithPersonItemAsset | null> => {
const assignment = await db.assignment.findUnique({
where: { id },
include: assignmentInclude,
})
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[]> => {
const assignments = await prisma.assignment.findMany({
where: { personId },
include: assignmentInclude,
})
return assignments.map(toLegacyAssignment)
},
create: async (
data: LegacyAssignmentWriteData,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Assignment> => {
const {
personId,
itemId,
assetId,
quantity,
assignmentDate,
createdBy,
createdById,
notes,
} = data
const assignment = await db.assignment.create({
data: {
personId,
notes,
assignedAt: assignmentDate,
createdById: createdById ?? createdBy ?? "",
stockLines:
!assetId && itemId
? {
create: {
itemId,
quantity,
notes,
},
}
: undefined,
assetLines: assetId
? {
create: {
assetId,
assignedAt: assignmentDate,
notes,
},
}
: undefined,
},
})
return toLegacyAssignmentRecord(assignment, { itemId, assetId, quantity })
},
delete: async (id: string): Promise<Assignment> => {
const closedAt = new Date()
return prisma.$transaction(async (tx) => {
const assignmentWithAssetLines = await tx.assignment.findUniqueOrThrow({
where: { id },
include: { assetLines: { include: { asset: true } } },
})
await Promise.all(
assignmentWithAssetLines.assetLines
.filter((line) => !line.returnedAt)
.map((line) =>
tx.assignmentAssetLine.update({
where: { id: line.id },
data: {
returnedAt: closedAt,
returnedById: assignmentWithAssetLines.createdById,
returnStatus: line.asset.status,
},
}),
),
)
const assignment = await tx.assignment.update({
where: { id },
data: {
status: "RETURNED",
closedAt,
},
})
return toLegacyAssignmentRecord(assignment, {})
})
},
markReturnedIfActive: async (
id: string,
actorId?: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<boolean> => {
const closedAt = new Date()
const assignment = await db.assignment.findFirst({
where: {
id,
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
},
include: {
stockLines: true,
assetLines: { include: { asset: true } },
},
})
if (!assignment) return false
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)
.map((line) =>
db.assignmentAssetLine.update({
where: { id: line.id },
data: {
returnedAt: closedAt,
returnedById: actorId ?? assignment.createdById,
returnStatus: line.asset.status,
},
}),
),
)
await db.assignment.update({
where: { id },
data: {
status: "RETURNED",
closedAt,
closedById: actorId,
},
})
return true
},
update: async (
id: string,
data: LegacyAssignmentUpdateData,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Assignment> => {
const {
itemId,
assetId,
quantity,
assignmentDate,
returnDate,
createdBy,
createdById,
...assignmentData
} = data
const assignment = await db.assignment.update({
where: { id },
data: {
...assignmentData,
assignedAt: assignmentDate,
closedAt: returnDate,
status: returnDate ? "RETURNED" : "OPEN",
createdById: createdById ?? createdBy,
},
})
if (itemId || quantity) {
await db.assignmentStockLine.updateMany({
where: { assignmentId: id },
data: {
itemId,
quantity,
},
})
}
if (assetId || returnDate) {
await db.assignmentAssetLine.updateMany({
where: { assignmentId: id },
data: {
assetId,
returnedAt: returnDate,
},
})
}
return toLegacyAssignmentRecord(assignment, { itemId, assetId, quantity })
},
}