refactor(items): move workflows into use cases
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Item_name_key" ON "Item"("name");
|
||||
@@ -88,7 +88,7 @@ enum ItemStatus {
|
||||
|
||||
model Item {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
name String @unique
|
||||
description String?
|
||||
categoryId String
|
||||
category Category @relation(fields: [categoryId], references: [id])
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
import {
|
||||
type CreateItemFormType,
|
||||
createItemSchema,
|
||||
type UpdateItemFormType,
|
||||
updateItemSchema,
|
||||
} from "@/schemas/item.schema"
|
||||
import { getAuthenticatedUserId } from "@/services/auth.service"
|
||||
import {
|
||||
createItemUseCase,
|
||||
deleteItemUseCase,
|
||||
updateItemUseCase,
|
||||
} from "@/use-cases/item.use-cases"
|
||||
|
||||
export async function createItemAction(formData: CreateItemFormType) {
|
||||
const validatedFields = createItemSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = await getAuthenticatedUserId()
|
||||
|
||||
const result = await createItemUseCase({
|
||||
...validatedFields.data,
|
||||
actorId: userId,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return result
|
||||
}
|
||||
|
||||
revalidatePath("/inventory/items")
|
||||
revalidatePath("/movements")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Item created successfully!",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
error: "Error creating item",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateItemAction(formData: UpdateItemFormType) {
|
||||
const validatedFields = updateItemSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = await getAuthenticatedUserId()
|
||||
|
||||
const result = await updateItemUseCase({
|
||||
...validatedFields.data,
|
||||
actorId: userId,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return result
|
||||
}
|
||||
|
||||
revalidatePath("/inventory/items")
|
||||
revalidatePath("/movements")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Item updated successfully!",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
error: "Failed to update item",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteItemAction(formData: FormData) {
|
||||
const { id } = Object.fromEntries(formData) as { id: string }
|
||||
|
||||
try {
|
||||
const result = await deleteItemUseCase(id)
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
...result,
|
||||
message: "Failed to delete item",
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath("/inventory/items")
|
||||
|
||||
return {
|
||||
success: true as const,
|
||||
message: "Item deleted successfully!",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
success: false as const,
|
||||
message: "Failed to delete item",
|
||||
errors: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,8 @@ import { Trash } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useTransition } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { deleteItemAction } from "@/actions/item.actions"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { deleteItemAction } from "@/lib/actions/item.actions"
|
||||
|
||||
export default function DeleteItemButton({ itemId }: { itemId: string }) {
|
||||
const router = useRouter()
|
||||
|
||||
@@ -4,14 +4,13 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { createItemAction } from "@/actions/item.actions"
|
||||
import { SubmitButton } from "@/components/forms/submitButton"
|
||||
import { createItemAction } from "@/lib/actions/item.actions"
|
||||
import {
|
||||
type CreateItemFormType,
|
||||
createItemSchema,
|
||||
} from "@/lib/schemas/item.schemas"
|
||||
import type { CategorySummary } from "@/lib/types"
|
||||
} from "@/schemas/item.schema"
|
||||
import type { CategorySummary } from "@/types"
|
||||
|
||||
export default function NewItemForm({
|
||||
categories,
|
||||
|
||||
@@ -4,14 +4,13 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { updateItemAction } from "@/actions/item.actions"
|
||||
import { SubmitButton } from "@/components/forms/submitButton"
|
||||
import { updateItemAction } from "@/lib/actions/item.actions"
|
||||
import {
|
||||
type UpdateItemFormType,
|
||||
updateItemSchema,
|
||||
} from "@/lib/schemas/item.schemas"
|
||||
import type { CategorySummary, ItemWithAssetCount } from "@/lib/types"
|
||||
} from "@/schemas/item.schema"
|
||||
import type { CategorySummary, ItemWithAssetCount } from "@/types"
|
||||
|
||||
export default function UpdateItemForm({
|
||||
categories,
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
import prisma from "@/lib/prisma"
|
||||
import {
|
||||
type CreateItemFormType,
|
||||
createItemSchema,
|
||||
type UpdateItemFormType,
|
||||
updateItemSchema,
|
||||
} from "@/lib/schemas/item.schemas"
|
||||
import { getAuthenticatedUserId } from "@/services/auth.service"
|
||||
import { ItemService } from "@/services/item.service"
|
||||
|
||||
export async function createItemAction(formData: CreateItemFormType) {
|
||||
const validatedFields = createItemSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { name, categoryId, stock } = validatedFields.data
|
||||
|
||||
if (stock < 0) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
stock: ["Stock cannot be negative"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const existingItem = await ItemService.findByName(name)
|
||||
|
||||
if (existingItem) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
name: ["An item with this name already exists"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const item = await ItemService.create({
|
||||
name,
|
||||
category: { connect: { id: categoryId } },
|
||||
stock: stock || 0,
|
||||
})
|
||||
|
||||
if (stock > 0) {
|
||||
await prisma.movement.create({
|
||||
data: {
|
||||
type: "IN",
|
||||
itemId: item.id,
|
||||
userId: await getAuthenticatedUserId(),
|
||||
quantity: stock,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
revalidatePath("/inventory/items")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Item created successfully!",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
error: "Error creating item",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateItemAction(formData: UpdateItemFormType) {
|
||||
const validatedFields = updateItemSchema.safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { id, stock, name, categoryId } = validatedFields.data
|
||||
|
||||
try {
|
||||
const existingItem = await ItemService.findByIdWithAssetCount(id)
|
||||
|
||||
if (!existingItem) {
|
||||
return {
|
||||
success: false,
|
||||
errors: {
|
||||
id: ["Item not found"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const existingItemByName = await ItemService.findByName(name)
|
||||
|
||||
if (existingItemByName && existingItemByName.id !== id) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { name: ["An item with this name already exists"] },
|
||||
}
|
||||
}
|
||||
|
||||
await ItemService.update(id, {
|
||||
stock: stock || existingItem.stock,
|
||||
name: name || existingItem.name,
|
||||
category: { connect: { id: categoryId } },
|
||||
})
|
||||
|
||||
const quantity = stock - existingItem.stock
|
||||
|
||||
if (stock && stock > existingItem.stock) {
|
||||
await prisma.movement.create({
|
||||
data: {
|
||||
type: "IN",
|
||||
itemId: id,
|
||||
quantity,
|
||||
userId: await getAuthenticatedUserId(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Item updated successfully!",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
error: "Failed to update item",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteItemAction(formData: FormData) {
|
||||
const { id } = Object.fromEntries(formData) as { id: string }
|
||||
|
||||
try {
|
||||
const existingItem = await ItemService.findByIdWithAssetCount(id)
|
||||
|
||||
if (!existingItem) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { id: ["Item not found"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (existingItem._count.assets > 0) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { id: ["Item has assets, you cannot delete it"] },
|
||||
}
|
||||
}
|
||||
|
||||
if (existingItem.stock > 0) {
|
||||
return {
|
||||
success: false,
|
||||
errors: { id: ["Item has stock, you cannot delete it"] },
|
||||
}
|
||||
}
|
||||
|
||||
await ItemService.delete(id)
|
||||
|
||||
revalidatePath("/inventory/items")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Item deleted successfully!",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return {
|
||||
error: "Failed to delete item",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,32 @@ import { z } from "zod"
|
||||
|
||||
export const createItemSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
error: "Name is required"
|
||||
}),
|
||||
error: "Name is required",
|
||||
}),
|
||||
categoryId: z.string().min(1, {
|
||||
error: "Category is required"
|
||||
}),
|
||||
stock: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.min(0, {
|
||||
error: "Stock is required"
|
||||
}),
|
||||
error: "Category is required",
|
||||
}),
|
||||
stock: z.coerce.number().int().nonnegative().min(0, {
|
||||
error: "Stock is required",
|
||||
}),
|
||||
})
|
||||
|
||||
export type CreateItemFormType = z.input<typeof createItemSchema>
|
||||
export type CreateItemData = z.output<typeof createItemSchema>
|
||||
|
||||
export const updateItemSchema = createItemSchema.extend({
|
||||
id: z.string().min(1, {
|
||||
error: "Item is required"
|
||||
}),
|
||||
error: "Item is required",
|
||||
}),
|
||||
})
|
||||
|
||||
export type UpdateItemFormType = z.input<typeof updateItemSchema>
|
||||
export type UpdateItemData = z.output<typeof updateItemSchema>
|
||||
|
||||
export const getItemByIdSchema = z.object({
|
||||
id: z.string().min(1, {
|
||||
error: "Item is required"
|
||||
}),
|
||||
error: "Item is required",
|
||||
}),
|
||||
})
|
||||
|
||||
export type GetItemByIdFormType = z.infer<typeof getItemByIdSchema>
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user