refactor(items): move workflows into use cases

This commit is contained in:
2026-06-04 22:11:40 +02:00
parent 2b908b24f6
commit 0af25417ab
9 changed files with 312 additions and 208 deletions
+173
View File
@@ -0,0 +1,173 @@
import { Prisma } from "@/generated/prisma/client"
import prisma from "@/lib/prisma"
import type { CreateItemData, UpdateItemData } from "@/schemas/item.schema"
import { ItemService } from "@/services/item.service"
import { MovementService } from "@/services/movement.service"
type FieldErrors = Record<string, string[]>
type CreateItemUseCaseInput = CreateItemData & {
actorId: string
}
type UpdateItemUseCaseInput = UpdateItemData & {
actorId: string
}
type ItemUseCaseResult =
| {
success: true
}
| {
success: false
errors: FieldErrors
}
function itemError(errors: FieldErrors): ItemUseCaseResult {
return {
success: false,
errors,
}
}
function isUniqueConstraintError(error: unknown) {
return (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === "P2002"
)
}
export async function createItemUseCase(
input: CreateItemUseCaseInput,
): Promise<ItemUseCaseResult> {
const { actorId, name, categoryId, stock } = input
if (stock < 0) {
return itemError({ stock: ["Stock cannot be negative"] })
}
try {
return await prisma.$transaction(async (tx) => {
const existingItem = await ItemService.findByName(name, tx)
if (existingItem) {
return itemError({
name: ["An item with this name already exists"],
})
}
const item = await ItemService.create(
{
name,
category: { connect: { id: categoryId } },
stock: stock || 0,
},
tx,
)
if (stock > 0) {
await MovementService.create(
{
type: "IN",
itemId: item.id,
userId: actorId,
quantity: stock,
},
tx,
)
}
return {
success: true,
}
})
} catch (error) {
if (isUniqueConstraintError(error)) {
return itemError({ name: ["An item with this name already exists"] })
}
throw error
}
}
export async function updateItemUseCase(
input: UpdateItemUseCaseInput,
): Promise<ItemUseCaseResult> {
const { actorId, id, stock, name, categoryId } = input
try {
return await prisma.$transaction(async (tx) => {
const existingItem = await ItemService.findByIdWithAssetCount(id, tx)
if (!existingItem) {
return itemError({ id: ["Item not found"] })
}
const existingItemByName = await ItemService.findByName(name, tx)
if (existingItemByName && existingItemByName.id !== id) {
return itemError({ name: ["An item with this name already exists"] })
}
await ItemService.update(
id,
{
stock: stock || existingItem.stock,
name: name || existingItem.name,
category: { connect: { id: categoryId } },
},
tx,
)
const quantity = stock - existingItem.stock
if (stock && stock > existingItem.stock) {
await MovementService.create(
{
type: "IN",
itemId: id,
quantity,
userId: actorId,
},
tx,
)
}
return {
success: true,
}
})
} catch (error) {
if (isUniqueConstraintError(error)) {
return itemError({ name: ["An item with this name already exists"] })
}
throw error
}
}
export async function deleteItemUseCase(
id: string,
): Promise<ItemUseCaseResult> {
return prisma.$transaction(async (tx) => {
const existingItem = await ItemService.findByIdWithAssetCount(id, tx)
if (!existingItem) {
return itemError({ id: ["Item not found"] })
}
if (existingItem._count.assets > 0) {
return itemError({ id: ["Item has assets, you cannot delete it"] })
}
if (existingItem.stock > 0) {
return itemError({ id: ["Item has stock, you cannot delete it"] })
}
await ItemService.delete(id, tx)
return {
success: true,
}
})
}