diff --git a/src/lib/asset-categories.ts b/src/lib/asset-categories.ts new file mode 100644 index 0000000..4cca1b5 --- /dev/null +++ b/src/lib/asset-categories.ts @@ -0,0 +1,107 @@ +import { z } from "zod"; +import { prisma } from "./prisma"; + +export type AssetCategoryValueGroup = "high_value" | "low_value"; + +export interface AssetCategoryRecord { + id: string; + name: string; + value_group: AssetCategoryValueGroup; + created_at: string; +} + +export interface CreateAssetCategoryInput { + name: string; + valueGroup: AssetCategoryValueGroup; +} + +export type ValidationResult = + | { ok: true; value: CreateAssetCategoryInput } + | { ok: false; error: string }; + +// Mirrors src/lib/vendors.ts's nonEmptyString helper. +function nonEmptyString(message: string) { + return z.string({ required_error: message, invalid_type_error: message }).refine( + (value) => value.trim() !== "", + { message } + ); +} + +const createAssetCategorySchema = z.object({ + name: nonEmptyString("name is required and must be a non-empty string"), + // Fixed 2-value set (see docs/asset-license-management.md's "## Phân + // loại tài sản" section) — mirrors src/lib/licenses.ts's licenseType + // z.enum() pattern. + valueGroup: z.enum(["high_value", "low_value"], { + errorMap: () => ({ + message: "valueGroup is required and must be either 'high_value' or 'low_value'", + }), + }), +}); + +/** + * Validates raw (untyped) asset-category-creation input. name must be a + * non-empty string, valueGroup must be one of the two fixed enum values + * (per the "## Phân loại tài sản" spec in docs/asset-license-management.md). + */ +export function validateCreateAssetCategoryInput(input: { + name?: unknown; + valueGroup?: unknown; +}): ValidationResult { + const result = createAssetCategorySchema.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; + name: string; + valueGroup: AssetCategoryValueGroup; + createdAt: Date; +}): AssetCategoryRecord { + return { + id: row.id, + name: row.name, + value_group: row.valueGroup, + created_at: row.createdAt.toISOString(), + }; +} + +export type CreateAssetCategoryResult = + | { ok: true; value: AssetCategoryRecord } + | { ok: false; error: string }; + +/** + * Creates an AssetCategory. No unique constraint on any field, no FKs + * either (see docs/asset-license-management.md's "## Phân loại tài sản" + * spec — standalone entity, same starting shape as Vendor before it grew + * relations) — so there's no known Prisma error code to catch/translate + * here today. Still returns the `{ok,value}|{ok,error}` shape (rather than + * a bare Promise like createVendor()) to match the + * convention used across the newer entities (AssetItem/License) — see the + * code-reviewer finding on createLicense() in CLAUDE.md's "Trạng thái hiện + * tại": adding this shape up front avoids a silent 500 if a unique + * constraint is added later (e.g. on `name`) without remembering to also + * add P2002 handling here. + */ +export async function createAssetCategory( + input: CreateAssetCategoryInput +): Promise { + const created = await prisma.assetCategory.create({ + data: { + name: input.name, + valueGroup: input.valueGroup, + }, + }); + return { ok: true, value: toRecord(created) }; +} + +export async function listAssetCategories(): Promise { + const rows = await prisma.assetCategory.findMany({ + orderBy: [{ createdAt: "desc" }, { id: "desc" }], + }); + return rows.map(toRecord); +}