feat(inventory): support line-based assignments and movements
This commit is contained in:
@@ -248,7 +248,13 @@ export async function importItems(formData: ImportFormType) {
|
||||
|
||||
if (!existingItem) {
|
||||
newItem = await ItemService.create({
|
||||
sku: name
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, ""),
|
||||
name,
|
||||
trackingType: "QUANTITY",
|
||||
stock: assigned ? 0 : stock || 0,
|
||||
category: {
|
||||
connect: { id: categoryId ? categoryId : newCategory?.id || "" },
|
||||
|
||||
@@ -17,11 +17,12 @@ const createAssetStatuses = ["AVAILABLE", "ASSIGNED"] as const
|
||||
const updateAssetStatuses = [
|
||||
"AVAILABLE",
|
||||
"ASSIGNED",
|
||||
"RESERVED",
|
||||
"IN_REPAIR",
|
||||
"BROKEN",
|
||||
"LOST",
|
||||
"STOLEN",
|
||||
"DISPOSED",
|
||||
"RETIRED",
|
||||
] as const
|
||||
|
||||
function buildAssetBaseSchema(copy: AssetSchemaCopy) {
|
||||
|
||||
@@ -6,6 +6,35 @@ import type {
|
||||
AssetWithItemAndCategory,
|
||||
} from "@/types/asset"
|
||||
|
||||
type AssetWithActiveAssignmentLine = Prisma.AssetGetPayload<{
|
||||
include: {
|
||||
item: true
|
||||
assignmentLines: { include: { assignment: true } }
|
||||
}
|
||||
}>
|
||||
|
||||
function toAssetWithAssignment(
|
||||
asset: AssetWithActiveAssignmentLine | null,
|
||||
): AssetWithAssignment | null {
|
||||
if (!asset) return null
|
||||
|
||||
const activeAssignment = asset.assignmentLines[0]?.assignment ?? null
|
||||
|
||||
return {
|
||||
...asset,
|
||||
assignment: activeAssignment
|
||||
? {
|
||||
...activeAssignment,
|
||||
assignmentDate: activeAssignment.assignedAt,
|
||||
returnDate: activeAssignment.closedAt,
|
||||
itemId: asset.itemId,
|
||||
assetId: asset.id,
|
||||
quantity: 1,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
export const AssetService = {
|
||||
findAll: async (opts?: {
|
||||
includeItem?: boolean
|
||||
@@ -97,10 +126,20 @@ export const AssetService = {
|
||||
id: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<AssetWithAssignment | null> => {
|
||||
return db.asset.findUnique({
|
||||
const asset = await db.asset.findUnique({
|
||||
where: { id },
|
||||
include: { item: true, assignment: true },
|
||||
include: {
|
||||
item: true,
|
||||
assignmentLines: {
|
||||
where: { returnedAt: null },
|
||||
include: { assignment: true },
|
||||
orderBy: { assignedAt: "desc" },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return toAssetWithAssignment(asset)
|
||||
},
|
||||
|
||||
findByItemId: async (itemId: string): Promise<Asset[]> => {
|
||||
@@ -111,10 +150,20 @@ export const AssetService = {
|
||||
serialNumber: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<AssetWithAssignment | null> => {
|
||||
return db.asset.findUnique({
|
||||
const asset = await db.asset.findUnique({
|
||||
where: { serialNumber },
|
||||
include: { item: true, assignment: true },
|
||||
include: {
|
||||
item: true,
|
||||
assignmentLines: {
|
||||
where: { returnedAt: null },
|
||||
include: { assignment: true },
|
||||
orderBy: { assignedAt: "desc" },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return toAssetWithAssignment(asset)
|
||||
},
|
||||
|
||||
create: async (
|
||||
|
||||
@@ -4,23 +4,79 @@ import prisma from "@/lib/prisma"
|
||||
import type { CreateAssignmentData } from "@/schemas/assignment.schema"
|
||||
import type { Assignment, AssignmentWithPersonItemAsset } from "@/types"
|
||||
|
||||
type LegacyAssignmentWriteData = CreateAssignmentData & {
|
||||
createdBy?: string
|
||||
createdById?: string
|
||||
}
|
||||
|
||||
type LegacyAssignmentUpdateData = Partial<LegacyAssignmentWriteData> & {
|
||||
returnDate?: Date | null
|
||||
}
|
||||
|
||||
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 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<{}>,
|
||||
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[]> => {
|
||||
return prisma.assignment.findMany({
|
||||
const assignments = await prisma.assignment.findMany({
|
||||
where: {
|
||||
returnDate: {
|
||||
equals: null,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
person: true,
|
||||
item: true,
|
||||
asset: true,
|
||||
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
|
||||
},
|
||||
include: assignmentInclude,
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
})
|
||||
|
||||
return assignments.map(toLegacyAssignment)
|
||||
},
|
||||
findAllWithPersonPaginated: async ({
|
||||
page,
|
||||
@@ -31,14 +87,12 @@ export const AssignmentService = {
|
||||
pageSize?: number
|
||||
search?: string
|
||||
}) => {
|
||||
return paginate<AssignmentWithPersonItemAsset>({
|
||||
const result = await paginate<AssignmentWithLines>({
|
||||
model: prisma.assignment,
|
||||
page,
|
||||
pageSize,
|
||||
where: {
|
||||
returnDate: {
|
||||
equals: null,
|
||||
},
|
||||
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
|
||||
...(search
|
||||
? {
|
||||
OR: [
|
||||
@@ -56,93 +110,213 @@ export const AssignmentService = {
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
include: {
|
||||
person: true,
|
||||
item: true,
|
||||
asset: true,
|
||||
},
|
||||
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> => {
|
||||
return db.assignment.findUnique({
|
||||
const assignment = await db.assignment.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
person: true,
|
||||
item: true,
|
||||
asset: true,
|
||||
},
|
||||
include: assignmentInclude,
|
||||
})
|
||||
|
||||
return assignment ? toLegacyAssignment(assignment) : null
|
||||
},
|
||||
findAllByPerson: async (
|
||||
personId: string,
|
||||
): Promise<AssignmentWithPersonItemAsset[]> => {
|
||||
return prisma.assignment.findMany({
|
||||
where: { personId: personId },
|
||||
include: {
|
||||
person: true,
|
||||
item: true,
|
||||
asset: true,
|
||||
},
|
||||
const assignments = await prisma.assignment.findMany({
|
||||
where: { personId },
|
||||
include: assignmentInclude,
|
||||
})
|
||||
|
||||
return assignments.map(toLegacyAssignment)
|
||||
},
|
||||
create: async (
|
||||
data: CreateAssignmentData & { createdBy: string },
|
||||
data: LegacyAssignmentWriteData,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Assignment> => {
|
||||
const { personId, ...rest } = data
|
||||
return db.assignment.create({
|
||||
const {
|
||||
personId,
|
||||
itemId,
|
||||
assetId,
|
||||
quantity,
|
||||
assignmentDate,
|
||||
createdBy,
|
||||
createdById,
|
||||
notes,
|
||||
} = data
|
||||
|
||||
const assignment = await db.assignment.create({
|
||||
data: {
|
||||
...rest,
|
||||
personId: personId,
|
||||
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> => {
|
||||
return prisma.assignment.update({
|
||||
where: { id },
|
||||
data: {
|
||||
returnDate: new Date(),
|
||||
personId: null,
|
||||
quantity: null,
|
||||
assetId: null,
|
||||
itemId: null,
|
||||
},
|
||||
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 result = await db.assignment.updateMany({
|
||||
const closedAt = new Date()
|
||||
const assignment = await db.assignment.findFirst({
|
||||
where: {
|
||||
id,
|
||||
returnDate: null,
|
||||
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
|
||||
},
|
||||
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 },
|
||||
}),
|
||||
),
|
||||
)
|
||||
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: {
|
||||
returnDate: new Date(),
|
||||
personId: null,
|
||||
quantity: null,
|
||||
assetId: null,
|
||||
itemId: null,
|
||||
status: "RETURNED",
|
||||
closedAt,
|
||||
closedById: actorId,
|
||||
},
|
||||
})
|
||||
|
||||
return result.count === 1
|
||||
return true
|
||||
},
|
||||
update: async (
|
||||
id: string,
|
||||
data: Prisma.AssignmentUpdateInput | Prisma.AssignmentUncheckedUpdateInput,
|
||||
data: LegacyAssignmentUpdateData,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Assignment> => {
|
||||
return db.assignment.update({
|
||||
const {
|
||||
itemId,
|
||||
assetId,
|
||||
quantity,
|
||||
assignmentDate,
|
||||
returnDate,
|
||||
createdBy,
|
||||
createdById,
|
||||
...assignmentData
|
||||
} = data
|
||||
const assignment = await db.assignment.update({
|
||||
where: { id },
|
||||
data,
|
||||
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 })
|
||||
},
|
||||
}
|
||||
|
||||
@@ -56,8 +56,14 @@ export const ItemService = {
|
||||
assets: opts?.includeAssets
|
||||
? { select: { id: true, serialNumber: true, status: true } }
|
||||
: false,
|
||||
movements: opts?.includeMovements
|
||||
? { select: { id: true, type: true, quantity: true } }
|
||||
stockMovementLines: opts?.includeMovements
|
||||
? {
|
||||
select: {
|
||||
id: true,
|
||||
stockDelta: true,
|
||||
movement: { select: { type: true } },
|
||||
},
|
||||
}
|
||||
: false,
|
||||
},
|
||||
})
|
||||
@@ -114,13 +120,23 @@ export const ItemService = {
|
||||
findByIdWithAssetAndMovementCount: async (
|
||||
id: string,
|
||||
): Promise<ItemWithAssetAndMovementCount | null> => {
|
||||
return prisma.item.findUnique({
|
||||
const item = await prisma.item.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
category: { select: { id: true, name: true } },
|
||||
_count: { select: { assets: true, movements: true } },
|
||||
_count: { select: { assets: true, stockMovementLines: true } },
|
||||
},
|
||||
})
|
||||
|
||||
return item
|
||||
? {
|
||||
...item,
|
||||
_count: {
|
||||
assets: item._count.assets,
|
||||
movements: item._count.stockMovementLines,
|
||||
},
|
||||
}
|
||||
: null
|
||||
},
|
||||
|
||||
findByName: async (
|
||||
@@ -129,7 +145,7 @@ export const ItemService = {
|
||||
): Promise<Item | null> => {
|
||||
return db.item.findFirst({
|
||||
where: { name },
|
||||
include: { category: true, assets: true, movements: true },
|
||||
include: { category: true, assets: true, stockMovementLines: true },
|
||||
}) as Promise<Item | null>
|
||||
},
|
||||
|
||||
@@ -139,7 +155,7 @@ export const ItemService = {
|
||||
): Promise<Item | null> => {
|
||||
return db.item.findUnique({
|
||||
where: { id },
|
||||
include: { category: true, assets: true, movements: true },
|
||||
include: { category: true, assets: true, stockMovementLines: true },
|
||||
}) as Promise<Item | null>
|
||||
},
|
||||
|
||||
@@ -153,7 +169,13 @@ export const ItemService = {
|
||||
include: {
|
||||
category: true,
|
||||
assets: { select: { id: true, serialNumber: true, status: true } },
|
||||
movements: { select: { id: true, type: true, quantity: true } },
|
||||
stockMovementLines: {
|
||||
select: {
|
||||
id: true,
|
||||
stockDelta: true,
|
||||
movement: { select: { type: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as Promise<Item[]>
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MovementType, Prisma } from "@/generated/prisma/client"
|
||||
import type { InventoryMovementType, Prisma } from "@/generated/prisma/client"
|
||||
import { paginate } from "@/lib/paginate"
|
||||
import prisma from "@/lib/prisma"
|
||||
import type { CreateMovementFormType } from "@/schemas/movement.schema"
|
||||
@@ -6,7 +6,7 @@ import type { Movement } from "@/types"
|
||||
|
||||
type MovementListResult = {
|
||||
id: string
|
||||
type: MovementType
|
||||
type: string
|
||||
quantity: number
|
||||
createdAt: Date
|
||||
item: { name: string } | null
|
||||
@@ -14,65 +14,283 @@ type MovementListResult = {
|
||||
person: { firstName: string; lastName: string } | null
|
||||
}
|
||||
|
||||
type InventoryMovementWithDetails = {
|
||||
id: string
|
||||
type: InventoryMovementType
|
||||
createdAt: Date
|
||||
stockLines: { stockDelta: number; item: { name: string } }[]
|
||||
assetLines: {
|
||||
asset: { serialNumber: string; item: { name: string } }
|
||||
}[]
|
||||
assignment: { person: { firstName: string; lastName: string } } | null
|
||||
}
|
||||
|
||||
const movementTypeMap = {
|
||||
IN: "RECEIPT",
|
||||
OUT: "ISSUE",
|
||||
ASSIGNMENT: "ASSIGNMENT",
|
||||
RETURN: "RETURN",
|
||||
ADJUSTMENT: "ADJUSTMENT",
|
||||
} as const satisfies Record<
|
||||
CreateMovementFormType["type"],
|
||||
InventoryMovementType
|
||||
>
|
||||
|
||||
const movementReasonMap = {
|
||||
IN: "MANUAL_ENTRY",
|
||||
OUT: "OTHER",
|
||||
ASSIGNMENT: "EMPLOYEE_ASSIGNMENT",
|
||||
RETURN: "EMPLOYEE_RETURN",
|
||||
ADJUSTMENT: "INVENTORY_CORRECTION",
|
||||
} as const satisfies Record<
|
||||
CreateMovementFormType["type"],
|
||||
Prisma.InventoryMovementCreateInput["reason"]
|
||||
>
|
||||
|
||||
const stockDeltaSignMap = {
|
||||
IN: 1,
|
||||
OUT: -1,
|
||||
ASSIGNMENT: -1,
|
||||
RETURN: 1,
|
||||
ADJUSTMENT: 1,
|
||||
} as const satisfies Record<CreateMovementFormType["type"], 1 | -1>
|
||||
|
||||
function toLegacyMovementType(type: InventoryMovementType) {
|
||||
if (type === "RECEIPT") return "IN"
|
||||
if (type === "ISSUE") return "OUT"
|
||||
return type
|
||||
}
|
||||
|
||||
function getStockSnapshot(currentStock: number, stockDelta: number) {
|
||||
if (stockDelta < 0) {
|
||||
return {
|
||||
previousStock: currentStock + Math.abs(stockDelta),
|
||||
newStock: currentStock,
|
||||
}
|
||||
}
|
||||
|
||||
const previousStock = Math.max(currentStock - stockDelta, 0)
|
||||
|
||||
return {
|
||||
previousStock,
|
||||
newStock: previousStock + stockDelta,
|
||||
}
|
||||
}
|
||||
|
||||
function getMovementQuantity(movement: {
|
||||
stockLines?: { stockDelta: number }[]
|
||||
assetLines?: unknown[]
|
||||
}) {
|
||||
const stockQuantity = movement.stockLines?.reduce(
|
||||
(total, line) => total + Math.abs(line.stockDelta),
|
||||
0,
|
||||
)
|
||||
|
||||
return stockQuantity || movement.assetLines?.length || 0
|
||||
}
|
||||
|
||||
function getMovementItem(movement: {
|
||||
stockLines?: { item: { name: string } }[]
|
||||
assetLines?: { asset: { item: { name: string } } }[]
|
||||
}) {
|
||||
return (
|
||||
movement.stockLines?.[0]?.item ||
|
||||
movement.assetLines?.[0]?.asset.item ||
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
function getMovementAsset(movement: {
|
||||
assetLines?: { asset: { serialNumber: string } }[]
|
||||
}) {
|
||||
return movement.assetLines?.[0]?.asset || null
|
||||
}
|
||||
|
||||
function getMovementPerson(movement: {
|
||||
assignment?: { person: { firstName: string; lastName: string } } | null
|
||||
}) {
|
||||
return movement.assignment?.person || null
|
||||
}
|
||||
|
||||
export const MovementService = {
|
||||
findAll: async ({ page, pageSize }: { page?: number; pageSize?: number }) => {
|
||||
return paginate<MovementListResult>({
|
||||
model: prisma.movement,
|
||||
const result = await paginate<InventoryMovementWithDetails>({
|
||||
model: prisma.inventoryMovement,
|
||||
page,
|
||||
pageSize,
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
quantity: true,
|
||||
createdAt: true,
|
||||
item: {
|
||||
select: {
|
||||
name: true,
|
||||
include: {
|
||||
stockLines: {
|
||||
include: {
|
||||
item: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
asset: {
|
||||
select: {
|
||||
serialNumber: true,
|
||||
assetLines: {
|
||||
include: {
|
||||
asset: {
|
||||
select: {
|
||||
serialNumber: true,
|
||||
item: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
person: {
|
||||
assignment: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
person: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data.map(
|
||||
(movement): MovementListResult => ({
|
||||
id: movement.id,
|
||||
type: toLegacyMovementType(movement.type),
|
||||
quantity: getMovementQuantity(movement),
|
||||
createdAt: movement.createdAt,
|
||||
item: getMovementItem(movement),
|
||||
asset: getMovementAsset(movement),
|
||||
person: getMovementPerson(movement),
|
||||
}),
|
||||
),
|
||||
}
|
||||
},
|
||||
create: async (
|
||||
data: CreateMovementFormType & { userId: string },
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Movement> => {
|
||||
const { personId, ...rest } = data
|
||||
return await db.movement.create({
|
||||
const {
|
||||
assetId,
|
||||
itemId,
|
||||
personId: _personId,
|
||||
quantity,
|
||||
type,
|
||||
userId,
|
||||
...rest
|
||||
} = data
|
||||
const stockDelta = quantity * stockDeltaSignMap[type]
|
||||
const item = itemId
|
||||
? await db.item.findUnique({
|
||||
where: { id: itemId },
|
||||
select: { stock: true },
|
||||
})
|
||||
: null
|
||||
const asset = assetId
|
||||
? await db.asset.findUnique({
|
||||
where: { id: assetId },
|
||||
select: { status: true },
|
||||
})
|
||||
: null
|
||||
|
||||
return await db.inventoryMovement.create({
|
||||
data: {
|
||||
...rest,
|
||||
personId: personId,
|
||||
type: movementTypeMap[type],
|
||||
reason: movementReasonMap[type],
|
||||
performedById: userId,
|
||||
stockLines:
|
||||
itemId && item
|
||||
? {
|
||||
create: {
|
||||
itemId,
|
||||
stockDelta,
|
||||
...getStockSnapshot(item.stock, stockDelta),
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
assetLines:
|
||||
assetId && asset
|
||||
? {
|
||||
create: {
|
||||
assetId,
|
||||
newStatus: asset.status,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
update: async (
|
||||
id: string,
|
||||
data: Prisma.MovementUpdateInput,
|
||||
data: Prisma.InventoryMovementUpdateInput,
|
||||
): Promise<Movement> => {
|
||||
return await prisma.movement.update({
|
||||
return await prisma.inventoryMovement.update({
|
||||
where: { id },
|
||||
data,
|
||||
})
|
||||
},
|
||||
|
||||
findAllByItemId: async (itemId: string): Promise<Movement[]> => {
|
||||
return await prisma.movement.findMany({
|
||||
where: { itemId },
|
||||
findAllByItemId: async (itemId: string): Promise<MovementListResult[]> => {
|
||||
const movements = await prisma.inventoryMovement.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ stockLines: { some: { itemId } } },
|
||||
{ assetLines: { some: { asset: { itemId } } } },
|
||||
],
|
||||
},
|
||||
include: {
|
||||
stockLines: {
|
||||
where: { itemId },
|
||||
include: {
|
||||
item: {
|
||||
select: { name: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
assetLines: {
|
||||
where: { asset: { itemId } },
|
||||
include: {
|
||||
asset: {
|
||||
select: {
|
||||
serialNumber: true,
|
||||
item: {
|
||||
select: { name: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
assignment: {
|
||||
select: {
|
||||
person: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
})
|
||||
|
||||
return movements.map((movement) => ({
|
||||
id: movement.id,
|
||||
type: toLegacyMovementType(movement.type),
|
||||
quantity: getMovementQuantity(movement),
|
||||
createdAt: movement.createdAt,
|
||||
item: getMovementItem(movement),
|
||||
asset: getMovementAsset(movement),
|
||||
person: getMovementPerson(movement),
|
||||
}))
|
||||
},
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,14 +1,15 @@
|
||||
import type {
|
||||
Assignment,
|
||||
Asset as PrismaAsset,
|
||||
ItemStatus as PrismaItemStatus,
|
||||
AssetStatus as PrismaAssetStatus,
|
||||
} from "@/generated/prisma/client"
|
||||
|
||||
import type { Assignment } from "./assignment"
|
||||
|
||||
export type Asset = PrismaAsset
|
||||
|
||||
export type ItemStatus = PrismaItemStatus
|
||||
export type ItemStatus = PrismaAssetStatus
|
||||
|
||||
export type UpdateAssetStatus = PrismaItemStatus
|
||||
export type UpdateAssetStatus = PrismaAssetStatus
|
||||
|
||||
export type AssetWithAssignment = Asset & {
|
||||
assignment: Assignment | null
|
||||
|
||||
+11
-1
@@ -4,13 +4,23 @@ import type { Asset } from "./asset"
|
||||
import type { Item } from "./item"
|
||||
import type { Person } from "./person"
|
||||
|
||||
export type Assignment = PrismaAssignment
|
||||
export type Assignment = PrismaAssignment & {
|
||||
assignmentDate: Date
|
||||
returnDate: Date | null
|
||||
itemId: string | null
|
||||
assetId: string | null
|
||||
quantity: number | null
|
||||
}
|
||||
|
||||
export type AssignmentSummary = Pick<Assignment, "id" | "quantity">
|
||||
|
||||
export type AssignmentWithPersonItemAsset = Assignment & {
|
||||
assignmentDate: Date
|
||||
returnDate: Date | null
|
||||
person: Person | null
|
||||
item: Item | null
|
||||
asset: Asset | null
|
||||
itemId: string | null
|
||||
assetId: string | null
|
||||
quantity: number | null
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { Movement as PrismaMovement } from "@/generated/prisma/client"
|
||||
import type { InventoryMovement as PrismaInventoryMovement } from "@/generated/prisma/client"
|
||||
|
||||
export type Movement = PrismaMovement
|
||||
export type Movement = PrismaInventoryMovement
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
type Assignment,
|
||||
type ItemStatus,
|
||||
type AssetStatus,
|
||||
Prisma,
|
||||
} from "@/generated/prisma/client"
|
||||
import prisma from "@/lib/prisma"
|
||||
@@ -12,6 +11,7 @@ import { AssetService } from "@/services/asset.service"
|
||||
import { AssignmentService } from "@/services/assignment.service"
|
||||
import { ItemService } from "@/services/item.service"
|
||||
import { MovementService } from "@/services/movement.service"
|
||||
import type { Assignment } from "@/types"
|
||||
|
||||
type FieldErrors = Record<string, string[]>
|
||||
|
||||
@@ -63,8 +63,8 @@ class AssetTransitionError extends Error {
|
||||
}
|
||||
|
||||
type AssetTransitionInput = {
|
||||
previousStatus: ItemStatus
|
||||
nextStatus: ItemStatus
|
||||
previousStatus: AssetStatus
|
||||
nextStatus: AssetStatus
|
||||
previousItemId: string | null
|
||||
nextItemId: string
|
||||
activeAssignment: Assignment | null
|
||||
@@ -261,7 +261,11 @@ export async function updateAssetUseCase(
|
||||
if (transition.activeAssignment && !transition.willBeAssigned) {
|
||||
const activeAssignment = transition.activeAssignment
|
||||
const assignmentWasReturned =
|
||||
await AssignmentService.markReturnedIfActive(activeAssignment.id, tx)
|
||||
await AssignmentService.markReturnedIfActive(
|
||||
activeAssignment.id,
|
||||
actorId,
|
||||
tx,
|
||||
)
|
||||
|
||||
if (!assignmentWasReturned) {
|
||||
throw new AssetTransitionError({
|
||||
|
||||
@@ -301,6 +301,7 @@ export async function returnAssignmentUseCase(
|
||||
|
||||
const assignmentWasReturned = await AssignmentService.markReturnedIfActive(
|
||||
id,
|
||||
actorId,
|
||||
tx,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,6 +37,14 @@ function isUniqueConstraintError(error: unknown) {
|
||||
)
|
||||
}
|
||||
|
||||
function buildSkuFromName(name: string) {
|
||||
return name
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
}
|
||||
|
||||
export async function createItemUseCase(
|
||||
input: CreateItemUseCaseInput,
|
||||
): Promise<ItemUseCaseResult> {
|
||||
@@ -58,7 +66,9 @@ export async function createItemUseCase(
|
||||
|
||||
const item = await ItemService.create(
|
||||
{
|
||||
sku: buildSkuFromName(name),
|
||||
name,
|
||||
trackingType: "QUANTITY",
|
||||
category: { connect: { id: categoryId } },
|
||||
stock: stock || 0,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,8 @@ import type {
|
||||
PrismaClient,
|
||||
UserRole,
|
||||
} from "@/generated/prisma/client"
|
||||
import { UserStatus } from "@/generated/prisma/client"
|
||||
import { normalizeEmail } from "@/lib/email"
|
||||
|
||||
let sequence = 0
|
||||
|
||||
@@ -21,14 +23,20 @@ export async function createTestUser(
|
||||
}> = {},
|
||||
) {
|
||||
const suffix = nextSuffix()
|
||||
const email = overrides.email ?? `test-user-${suffix}@example.test`
|
||||
const status =
|
||||
(overrides.isActive ?? true) ? UserStatus.ACTIVE : UserStatus.DISABLED
|
||||
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
email: overrides.email ?? `test-user-${suffix}@example.test`,
|
||||
email,
|
||||
emailNormalized: normalizeEmail(email),
|
||||
name: overrides.name ?? "Test User",
|
||||
password: "hashed-password",
|
||||
passwordHash: "hashed-password",
|
||||
role: overrides.role ?? "ADMIN",
|
||||
isActive: overrides.isActive ?? true,
|
||||
status,
|
||||
...(status === UserStatus.ACTIVE ? { activatedAt: new Date() } : {}),
|
||||
passwordChangedAt: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -83,7 +91,9 @@ export async function createTestItem(
|
||||
|
||||
return prisma.item.create({
|
||||
data: {
|
||||
sku: `TEST-SKU-${suffix}`,
|
||||
name: overrides.name ?? `Test Item ${suffix}`,
|
||||
trackingType: "QUANTITY",
|
||||
stock: overrides.stock ?? 0,
|
||||
category: { connect: { id: categoryId } },
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ type TestDatabaseState = {
|
||||
const state: TestDatabaseState = {}
|
||||
|
||||
const TABLES_TO_TRUNCATE = [
|
||||
"Movement",
|
||||
"InventoryMovement",
|
||||
"Assignment",
|
||||
"Asset",
|
||||
"Item",
|
||||
|
||||
@@ -55,7 +55,8 @@ describe("asset use-cases", () => {
|
||||
const [asset, updatedItem, movements] = await Promise.all([
|
||||
prisma.asset.findUniqueOrThrow({ where: { id: result.assetId } }),
|
||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
prisma.movement.findMany({
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true, assetLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
}),
|
||||
])
|
||||
@@ -70,12 +71,11 @@ describe("asset use-cases", () => {
|
||||
expect(updatedItem.stock).toBe(1)
|
||||
expect(movements).toHaveLength(1)
|
||||
expect(movements[0]).toMatchObject({
|
||||
type: "IN",
|
||||
itemId: item.id,
|
||||
assetId: result.assetId,
|
||||
quantity: 1,
|
||||
userId: actor.id,
|
||||
type: "RECEIPT",
|
||||
performedById: actor.id,
|
||||
})
|
||||
expect(movements[0].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: 1 })
|
||||
expect(movements[0].assetLines[0]).toMatchObject({ assetId: result.assetId })
|
||||
})
|
||||
|
||||
it("creates an assigned asset with assignment and ASSIGNMENT movement", async () => {
|
||||
@@ -98,9 +98,14 @@ describe("asset use-cases", () => {
|
||||
prisma.asset.findUniqueOrThrow({ where: { id: result.assetId } }),
|
||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
prisma.assignment.findFirstOrThrow({
|
||||
where: { assetId: result.assetId, returnDate: null },
|
||||
where: {
|
||||
status: "OPEN",
|
||||
assetLines: { some: { assetId: result.assetId, returnedAt: null } },
|
||||
},
|
||||
include: { assetLines: true },
|
||||
}),
|
||||
prisma.movement.findMany({
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true, assetLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
}),
|
||||
])
|
||||
@@ -112,23 +117,19 @@ describe("asset use-cases", () => {
|
||||
})
|
||||
expect(updatedItem.stock).toBe(0)
|
||||
expect(assignment).toMatchObject({
|
||||
itemId: item.id,
|
||||
assetId: result.assetId,
|
||||
personId: person.id,
|
||||
quantity: 1,
|
||||
createdBy: actor.id,
|
||||
returnDate: null,
|
||||
createdById: actor.id,
|
||||
closedAt: null,
|
||||
})
|
||||
expect(assignment.assetLines[0]).toMatchObject({ assetId: result.assetId })
|
||||
expect(movements).toHaveLength(1)
|
||||
expect(movements[0]).toMatchObject({
|
||||
type: "ASSIGNMENT",
|
||||
itemId: item.id,
|
||||
assetId: result.assetId,
|
||||
personId: person.id,
|
||||
assignmentId: assignment.id,
|
||||
quantity: 1,
|
||||
userId: actor.id,
|
||||
performedById: actor.id,
|
||||
})
|
||||
expect(movements[0].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: -1 })
|
||||
expect(movements[0].assetLines[0]).toMatchObject({ assetId: result.assetId })
|
||||
})
|
||||
|
||||
it("moves an available asset to assigned and back to available", async () => {
|
||||
@@ -161,18 +162,20 @@ describe("asset use-cases", () => {
|
||||
prisma.asset.findUniqueOrThrow({ where: { id: created.assetId } }),
|
||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
prisma.assignment.findFirstOrThrow({
|
||||
where: { assetId: created.assetId, returnDate: null },
|
||||
where: {
|
||||
status: "OPEN",
|
||||
assetLines: { some: { assetId: created.assetId, returnedAt: null } },
|
||||
},
|
||||
include: { assetLines: true },
|
||||
}),
|
||||
])
|
||||
|
||||
expect(assignedAsset.status).toBe("ASSIGNED")
|
||||
expect(assignedItem.stock).toBe(0)
|
||||
expect(activeAssignment).toMatchObject({
|
||||
itemId: item.id,
|
||||
assetId: created.assetId,
|
||||
personId: person.id,
|
||||
quantity: 1,
|
||||
})
|
||||
expect(activeAssignment.assetLines[0]).toMatchObject({ assetId: created.assetId })
|
||||
|
||||
await expect(
|
||||
updateAssetUseCase({
|
||||
@@ -191,41 +194,37 @@ describe("asset use-cases", () => {
|
||||
prisma.assignment.findUniqueOrThrow({
|
||||
where: { id: activeAssignment.id },
|
||||
}),
|
||||
prisma.movement.findMany({
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true, assetLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
}),
|
||||
])
|
||||
|
||||
expect(availableAsset.status).toBe("AVAILABLE")
|
||||
expect(availableItem.stock).toBe(1)
|
||||
expect(returnedAssignment.returnDate).toBeInstanceOf(Date)
|
||||
expect(returnedAssignment.closedAt).toBeInstanceOf(Date)
|
||||
expect(returnedAssignment).toMatchObject({
|
||||
itemId: null,
|
||||
assetId: null,
|
||||
personId: null,
|
||||
quantity: null,
|
||||
personId: person.id,
|
||||
status: "RETURNED",
|
||||
})
|
||||
expect(movements).toHaveLength(3)
|
||||
expect(movements.map((movement) => movement.type)).toEqual([
|
||||
"IN",
|
||||
"RECEIPT",
|
||||
"ASSIGNMENT",
|
||||
"RETURN",
|
||||
])
|
||||
expect(movements[1]).toMatchObject({
|
||||
itemId: item.id,
|
||||
assetId: created.assetId,
|
||||
personId: person.id,
|
||||
assignmentId: activeAssignment.id,
|
||||
quantity: 1,
|
||||
userId: actor.id,
|
||||
performedById: actor.id,
|
||||
})
|
||||
expect(movements[1].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: -1 })
|
||||
expect(movements[1].assetLines[0]).toMatchObject({ assetId: created.assetId })
|
||||
expect(movements[2]).toMatchObject({
|
||||
itemId: item.id,
|
||||
assetId: created.assetId,
|
||||
assignmentId: activeAssignment.id,
|
||||
quantity: 1,
|
||||
userId: actor.id,
|
||||
performedById: actor.id,
|
||||
})
|
||||
expect(movements[2].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: 1 })
|
||||
expect(movements[2].assetLines[0]).toMatchObject({ assetId: created.assetId })
|
||||
})
|
||||
|
||||
it("returns an active assignment without restoring stock when an assigned asset moves to a terminal status", async () => {
|
||||
@@ -245,7 +244,10 @@ describe("asset use-cases", () => {
|
||||
if (!created.success) throw new Error("Expected asset creation success")
|
||||
|
||||
const activeAssignment = await prisma.assignment.findFirstOrThrow({
|
||||
where: { assetId: created.assetId, returnDate: null },
|
||||
where: {
|
||||
status: "OPEN",
|
||||
assetLines: { some: { assetId: created.assetId, returnedAt: null } },
|
||||
},
|
||||
})
|
||||
|
||||
await expect(
|
||||
@@ -265,14 +267,15 @@ describe("asset use-cases", () => {
|
||||
prisma.assignment.findUniqueOrThrow({
|
||||
where: { id: activeAssignment.id },
|
||||
}),
|
||||
prisma.movement.findMany({
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true, assetLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
}),
|
||||
])
|
||||
|
||||
expect(asset.status).toBe("BROKEN")
|
||||
expect(itemAfterUpdate.stock).toBe(0)
|
||||
expect(returnedAssignment.returnDate).toBeInstanceOf(Date)
|
||||
expect(returnedAssignment.closedAt).toBeInstanceOf(Date)
|
||||
expect(movements).toHaveLength(2)
|
||||
expect(movements.map((movement) => movement.type)).toEqual([
|
||||
"ASSIGNMENT",
|
||||
@@ -280,12 +283,10 @@ describe("asset use-cases", () => {
|
||||
])
|
||||
expect(movements[1]).toMatchObject({
|
||||
type: "RETURN",
|
||||
itemId: item.id,
|
||||
assetId: created.assetId,
|
||||
personId: person.id,
|
||||
assignmentId: activeAssignment.id,
|
||||
quantity: 1,
|
||||
userId: actor.id,
|
||||
performedById: actor.id,
|
||||
})
|
||||
expect(movements[1].stockLines[0]).toMatchObject({ itemId: item.id, stockDelta: 1 })
|
||||
expect(movements[1].assetLines[0]).toMatchObject({ assetId: created.assetId })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -59,30 +59,36 @@ describe("assignment use-cases", () => {
|
||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
prisma.assignment.findUniqueOrThrow({
|
||||
where: { id: result.assignmentId },
|
||||
include: { stockLines: true },
|
||||
}),
|
||||
prisma.movement.findMany({
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
}),
|
||||
])
|
||||
|
||||
expect(updatedItem.stock).toBe(3)
|
||||
expect(assignment).toMatchObject({
|
||||
itemId: item.id,
|
||||
personId: person.id,
|
||||
quantity: 2,
|
||||
notes: "Initial assignment",
|
||||
createdBy: actor.id,
|
||||
returnDate: null,
|
||||
createdById: actor.id,
|
||||
closedAt: null,
|
||||
})
|
||||
expect(assignment.assignedAt).toEqual(assignmentDate)
|
||||
expect(assignment.stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
quantity: 2,
|
||||
returnedQuantity: 0,
|
||||
})
|
||||
expect(assignment.assignmentDate).toEqual(assignmentDate)
|
||||
expect(movements).toHaveLength(1)
|
||||
expect(movements[0]).toMatchObject({
|
||||
type: "ASSIGNMENT",
|
||||
itemId: item.id,
|
||||
personId: person.id,
|
||||
assignmentId: result.assignmentId,
|
||||
quantity: 2,
|
||||
userId: actor.id,
|
||||
performedById: actor.id,
|
||||
})
|
||||
expect(movements[0].stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
stockDelta: -2,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -109,7 +115,7 @@ describe("assignment use-cases", () => {
|
||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
).resolves.toMatchObject({ stock: 1 })
|
||||
await expect(prisma.assignment.count()).resolves.toBe(0)
|
||||
await expect(prisma.movement.count()).resolves.toBe(0)
|
||||
await expect(prisma.inventoryMovement.count()).resolves.toBe(0)
|
||||
})
|
||||
|
||||
it("returns an assignment, restores stock, closes it, and records a RETURN movement", async () => {
|
||||
@@ -139,34 +145,43 @@ describe("assignment use-cases", () => {
|
||||
prisma.item.findUniqueOrThrow({ where: { id: item.id } }),
|
||||
prisma.assignment.findUniqueOrThrow({
|
||||
where: { id: created.assignmentId },
|
||||
include: { stockLines: true },
|
||||
}),
|
||||
prisma.movement.findMany({
|
||||
prisma.inventoryMovement.findMany({
|
||||
include: { stockLines: true },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
}),
|
||||
])
|
||||
|
||||
expect(updatedItem.stock).toBe(4)
|
||||
expect(assignment.returnDate).toBeInstanceOf(Date)
|
||||
expect(assignment.closedAt).toBeInstanceOf(Date)
|
||||
expect(assignment).toMatchObject({
|
||||
itemId: null,
|
||||
assetId: null,
|
||||
personId: null,
|
||||
quantity: null,
|
||||
personId: person.id,
|
||||
status: "RETURNED",
|
||||
})
|
||||
expect(assignment.stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
quantity: 3,
|
||||
returnedQuantity: 3,
|
||||
})
|
||||
expect(movements).toHaveLength(2)
|
||||
expect(movements[0]).toMatchObject({
|
||||
type: "ASSIGNMENT",
|
||||
itemId: item.id,
|
||||
assignmentId: created.assignmentId,
|
||||
quantity: 3,
|
||||
userId: actor.id,
|
||||
performedById: actor.id,
|
||||
})
|
||||
expect(movements[0].stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
stockDelta: -3,
|
||||
})
|
||||
expect(movements[1]).toMatchObject({
|
||||
type: "RETURN",
|
||||
itemId: item.id,
|
||||
assignmentId: created.assignmentId,
|
||||
quantity: 3,
|
||||
userId: actor.id,
|
||||
performedById: actor.id,
|
||||
})
|
||||
expect(movements[1].stockLines[0]).toMatchObject({
|
||||
itemId: item.id,
|
||||
stockDelta: 3,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -197,6 +212,6 @@ describe("assignment use-cases", () => {
|
||||
errors: { id: ["Assignment already returned"] },
|
||||
})
|
||||
|
||||
await expect(prisma.movement.count()).resolves.toBe(2)
|
||||
await expect(prisma.inventoryMovement.count()).resolves.toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,8 +46,8 @@ describe("item use-cases", () => {
|
||||
expect(result).toEqual({ success: true })
|
||||
|
||||
const item = await prisma.item.findUnique({
|
||||
where: { name: "Laptop" },
|
||||
include: { movements: true },
|
||||
where: { sku: "LAPTOP" },
|
||||
include: { stockMovementLines: { include: { movement: true } } },
|
||||
})
|
||||
|
||||
expect(item).toMatchObject({
|
||||
@@ -56,11 +56,13 @@ describe("item use-cases", () => {
|
||||
stock: 3,
|
||||
deletedAt: null,
|
||||
})
|
||||
expect(item?.movements).toHaveLength(1)
|
||||
expect(item?.movements[0]).toMatchObject({
|
||||
type: "IN",
|
||||
quantity: 3,
|
||||
userId: actor.id,
|
||||
expect(item?.stockMovementLines).toHaveLength(1)
|
||||
expect(item?.stockMovementLines[0]).toMatchObject({
|
||||
stockDelta: 3,
|
||||
})
|
||||
expect(item?.stockMovementLines[0]?.movement).toMatchObject({
|
||||
type: "RECEIPT",
|
||||
performedById: actor.id,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -102,7 +104,7 @@ describe("item use-cases", () => {
|
||||
})
|
||||
|
||||
const stockedItem = await prisma.item.findUniqueOrThrow({
|
||||
where: { name: "Keyboard" },
|
||||
where: { sku: "KEYBOARD" },
|
||||
})
|
||||
|
||||
await expect(deleteItemUseCase(stockedItem.id)).resolves.toEqual({
|
||||
@@ -118,7 +120,7 @@ describe("item use-cases", () => {
|
||||
})
|
||||
|
||||
const emptyItem = await prisma.item.findUniqueOrThrow({
|
||||
where: { name: "Mouse" },
|
||||
where: { sku: "MOUSE" },
|
||||
})
|
||||
|
||||
await expect(deleteItemUseCase(emptyItem.id)).resolves.toEqual({
|
||||
|
||||
Reference in New Issue
Block a user