import { z } from "zod"; import { Prisma } from "@prisma/client"; import { prisma } from "./prisma"; export type RequestType = "asset" | "license"; // Only the 'Draft' state is implemented in this task. Later stages // (Pending Approval, Approved, Ordered, Received, Delivered, Closed, // Rejected/Cancelled — see processes/asset-license-management.md) are // deliberately out of scope. export type RequestStatus = "Draft"; export interface RequestRecord { id: string; employee_id: string; type: RequestType; reason: string; status: RequestStatus; purchase_order_id: string | null; created_at: string; } export interface CreateRequestInput { employee_id: string; type: RequestType; reason: string; purchase_order_id?: string | null; } export type ValidationResult = | { ok: true; value: CreateRequestInput } | { ok: false; error: string }; // A string field that must be present and not just whitespace. Mirrors the // old hand-written `typeof x !== "string" || x.trim() === ""` check: the // value itself is NOT trimmed/transformed, only validated — the raw input is // still what gets stored. function nonEmptyString(message: string) { return z.string({ required_error: message, invalid_type_error: message }).refine( (value) => value.trim() !== "", { message } ); } const createRequestSchema = z.object({ // Now a real FK to Employee (see schema.prisma) — only a UUID *format* // check here — whether it references a real employee is left to Prisma's // own FK constraint at insert time (see createRequest()'s P2003 handling // below), rather than an extra pre-check query. Mirrors the same pattern // used for Employee.managerId in src/lib/employees.ts. employee_id: z .string({ required_error: "employee_id is required and must be a valid UUID string" }) .uuid("employee_id is required and must be a valid UUID string"), reason: nonEmptyString("reason is required and must be a non-empty string"), type: z.enum(["asset", "license"], { errorMap: () => ({ message: "type is required and must be exactly 'asset' or 'license'", }), }), // Nullable/optional FK to PurchaseOrder — see schema.prisma's comment on // Request.purchaseOrderId for the demand-vs-supply-side distinction from // AssetItem/License's purchaseOrderId. Only a UUID *format* check here, // whether it references a real PurchaseOrder is left to Prisma's own FK // constraint at insert time (see createRequest()'s P2003 handling below). // Mirrors src/lib/assets.ts's purchaseOrderId optional().nullable() // pattern exactly. purchase_order_id: z .string({ invalid_type_error: "purchase_order_id must be a valid UUID string" }) .uuid("purchase_order_id must be a valid UUID string") .optional() .nullable(), }); /** * Validates raw (untyped) request-creation input. * employee_id and reason must be non-empty strings; type must be exactly * 'asset' or 'license'. */ export function validateCreateRequestInput(input: { employee_id?: unknown; type?: unknown; reason?: unknown; purchase_order_id?: unknown; }): ValidationResult { const result = createRequestSchema.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; employeeId: string; type: RequestType; reason: string; status: string; purchaseOrderId: string | null; createdAt: Date; }): RequestRecord { return { id: row.id, employee_id: row.employeeId, type: row.type, reason: row.reason, status: row.status as RequestStatus, purchase_order_id: row.purchaseOrderId, created_at: row.createdAt.toISOString(), }; } export type CreateRequestResult = | { ok: true; value: RequestRecord } | { ok: false; error: string }; /** * Creates a Request. Two separate FKs can fail here (employee_id -> * Employee, purchase_order_id -> PurchaseOrder) — Prisma raises the same * error code (P2003) for either, so the FK constraint name in * `error.meta.field_name` is used to tell them apart and return a targeted * message, rather than a raw Prisma error leaking to the API layer. Mirrors * src/lib/licenses.ts's createLicense() pattern for disambiguating between * two FKs on the same model. */ export async function createRequest(input: CreateRequestInput): Promise { try { const created = await prisma.request.create({ data: { employeeId: input.employee_id, type: input.type, reason: input.reason, purchaseOrderId: input.purchase_order_id ?? null, }, }); return { ok: true, value: toRecord(created) }; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") { const fieldName = typeof error.meta?.field_name === "string" ? error.meta.field_name : ""; if (fieldName.includes("purchase_order")) { return { ok: false, error: "purchase_order_id does not reference an existing purchase order" }; } return { ok: false, error: "employee_id does not reference an existing employee" }; } throw error; } } export async function listRequests(): Promise { const rows = await prisma.request.findMany({ orderBy: [{ createdAt: "desc" }, { id: "desc" }], }); return rows.map(toRecord); }