import { z } from "zod"; import { Prisma } from "@prisma/client"; import { prisma } from "./prisma"; export type AssetAttributeDataType = "string" | "number" | "boolean" | "date"; export interface AssetCategoryAttributeDefRecord { id: string; category_id: string; key: string; label: string; data_type: AssetAttributeDataType; required: boolean; sort_order: number; created_at: string; } export interface CreateAssetCategoryAttributeDefInput { categoryId: string; key: string; label: string; dataType: AssetAttributeDataType; required: boolean; sortOrder: number; } export type ValidationResult = | { ok: true; value: CreateAssetCategoryAttributeDefInput } | { ok: false; error: string }; // Mirrors src/lib/asset-categories.ts's nonEmptyString helper. function nonEmptyString(message: string) { return z.string({ required_error: message, invalid_type_error: message }).refine( (value) => value.trim() !== "", { message } ); } const createAssetCategoryAttributeDefSchema = z.object({ // FK to AssetCategory — only a UUID *format* check here, whether it // references a real category is left to Prisma's own FK constraint at // insert time (see createAssetCategoryAttributeDef()'s P2003 handling // below). Mirrors src/lib/purchase-orders.ts's vendorId handling. categoryId: z .string({ required_error: "categoryId is required and must be a valid UUID string" }) .uuid("categoryId is required and must be a valid UUID string"), // Slug-style, e.g. "ram_gb" — only unique *within* a category (composite // unique (categoryId, key) at the DB layer), not globally. Just requires // non-empty here; the DB-level uniqueness is enforced by // createAssetCategoryAttributeDef()'s P2002 handling below. key: nonEmptyString("key is required and must be a non-empty string"), label: nonEmptyString("label is required and must be a non-empty string"), // Fixed 4-value set (see docs/asset-license-management.md's "Cập nhật // 2026-08-02 (3)" section) — a DEV-defined closed technical set, mirrors // src/lib/asset-categories.ts's valueGroup z.enum() pattern. dataType: z.enum(["string", "number", "boolean", "date"], { errorMap: () => ({ message: "dataType is required and must be one of 'string', 'number', 'boolean', 'date'", }), }), required: z.boolean({ required_error: "required is required and must be a boolean", invalid_type_error: "required is required and must be a boolean", }), sortOrder: z.number({ required_error: "sortOrder is required and must be an integer", invalid_type_error: "sortOrder is required and must be an integer", }) .int("sortOrder is required and must be an integer"), }); /** * Validates raw (untyped) asset-category-attribute-def-creation input. * categoryId must be a valid UUID string, key/label must be non-empty * strings, dataType must be one of the four fixed enum values, required * must be a boolean, sortOrder must be an integer. Whether categoryId * references a real AssetCategory, and whether (categoryId, key) is * actually unique, are both left to Prisma's constraints at insert time * (see createAssetCategoryAttributeDef() below). */ export function validateCreateAssetCategoryAttributeDefInput(input: { categoryId?: unknown; key?: unknown; label?: unknown; dataType?: unknown; required?: unknown; sortOrder?: unknown; }): ValidationResult { const result = createAssetCategoryAttributeDefSchema.safeParse(input); if (!result.success) { const message = result.error.issues[0]?.message ?? "Invalid input"; return { ok: false, error: message }; } return { ok: true, value: result.data }; } function toRecord(row: { id: string; categoryId: string; key: string; label: string; dataType: AssetAttributeDataType; required: boolean; sortOrder: number; createdAt: Date; }): AssetCategoryAttributeDefRecord { return { id: row.id, category_id: row.categoryId, key: row.key, label: row.label, data_type: row.dataType, required: row.required, sort_order: row.sortOrder, created_at: row.createdAt.toISOString(), }; } export type CreateAssetCategoryAttributeDefResult = | { ok: true; value: AssetCategoryAttributeDefRecord } | { ok: false; error: string }; /** * Creates an AssetCategoryAttributeDef. If (categoryId, key) collides with * an existing row, Prisma raises P2002 (composite unique constraint * violation) — caught here and translated into a clean {ok:false,error}, * same pattern as createAssetItem()'s serialNumber P2002 handling in * src/lib/assets.ts. Note this is a *composite* uniqueness (unique within a * category, not globally) — the same `key` under a *different* categoryId * is expected to succeed, per docs/asset-license-management.md's "Cập nhật * 2026-08-02 (3)" section. If categoryId doesn't reference an existing * AssetCategory, Prisma raises P2003 (FK constraint violation) — caught the * same way, mirrors src/lib/purchase-orders.ts's createPurchaseOrder() P2003 * handling. */ export async function createAssetCategoryAttributeDef( input: CreateAssetCategoryAttributeDefInput ): Promise { try { const created = await prisma.assetCategoryAttributeDef.create({ data: { categoryId: input.categoryId, key: input.key, label: input.label, dataType: input.dataType, required: input.required, sortOrder: input.sortOrder, }, }); return { ok: true, value: toRecord(created) }; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") { return { ok: false, error: "key already exists for this category" }; } if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") { return { ok: false, error: "categoryId does not reference an existing asset category" }; } throw error; } } export async function listAssetCategoryAttributeDefs(): Promise< AssetCategoryAttributeDefRecord[] > { const rows = await prisma.assetCategoryAttributeDef.findMany({ orderBy: [{ createdAt: "desc" }, { id: "desc" }], }); return rows.map(toRecord); }