import { z } from "zod"; import { Prisma } from "@prisma/client"; import { prisma } from "./prisma"; export interface PurchaseOrderRecord { id: string; vendor_id: string; order_date: string; expected_date: string; received_at: string | null; received_by: string | null; created_at: string; } export interface CreatePurchaseOrderInput { vendorId: string; orderDate: Date; expectedDate: Date; receivedAt?: Date | null; receivedById?: string | null; } export type ValidationResult = | { ok: true; value: CreatePurchaseOrderInput } | { ok: false; error: string }; // Mirrors src/lib/licenses.ts's dateString helper — accepts any string // Date.parse() can handle and transforms it into a Date for Prisma. Used // here for orderDate/expectedDate/receivedAt, which are all Timestamptz // (not just Date) columns, so datetime strings (not just "2026-07-31") are // also accepted. function dateString(message: string) { return z .string({ required_error: message, invalid_type_error: message }) .refine((value) => !Number.isNaN(Date.parse(value)), { message }) .transform((value) => new Date(value)); } const createPurchaseOrderSchema = z .object({ // FK to Vendor — only a UUID *format* check here, whether it references // a real vendor is left to Prisma's own FK constraint at insert time // (see createPurchaseOrder()'s P2003 handling below). Mirrors // src/lib/licenses.ts's vendorId handling. vendorId: z .string({ required_error: "vendorId is required and must be a valid UUID string" }) .uuid("vendorId is required and must be a valid UUID string"), orderDate: dateString("orderDate is required and must be a valid date string"), expectedDate: dateString("expectedDate is required and must be a valid date string"), // Both nullable — null means "not yet received". Co-nullability with // receivedById is enforced by the .refine() below, not here. receivedAt: dateString("receivedAt must be a valid date string").optional().nullable(), receivedById: z .string({ invalid_type_error: "receivedById must be a valid UUID string" }) .uuid("receivedById must be a valid UUID string") .optional() .nullable(), }) .refine( (value) => { const hasReceivedAt = value.receivedAt !== undefined && value.receivedAt !== null; const hasReceivedById = value.receivedById !== undefined && value.receivedById !== null; return hasReceivedAt === hasReceivedById; }, { message: "receivedAt and receivedById must both be set or both be unset", // Attach the error to both fields so it surfaces regardless of which // one the caller looks at first. path: ["receivedAt"], } ); /** * Validates raw (untyped) purchase-order-creation input. vendorId must be a * valid UUID string, orderDate/expectedDate must be valid date strings, * receivedAt/receivedById are optional/nullable but must both be set or * both be unset (co-nullability invariant — receiving a PO always records * both "when" and "who" together). No status field is accepted — PurchaseOrder * deliberately has no stored status (see schema.prisma comment), it's * inferred from linked Requests. */ export function validateCreatePurchaseOrderInput(input: { vendorId?: unknown; orderDate?: unknown; expectedDate?: unknown; receivedAt?: unknown; receivedById?: unknown; }): ValidationResult { const result = createPurchaseOrderSchema.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; vendorId: string; orderDate: Date; expectedDate: Date; receivedAt: Date | null; receivedById: string | null; createdAt: Date; }): PurchaseOrderRecord { return { id: row.id, vendor_id: row.vendorId, order_date: row.orderDate.toISOString(), expected_date: row.expectedDate.toISOString(), received_at: row.receivedAt ? row.receivedAt.toISOString() : null, received_by: row.receivedById, created_at: row.createdAt.toISOString(), }; } export type CreatePurchaseOrderResult = | { ok: true; value: PurchaseOrderRecord } | { ok: false; error: string }; /** * Creates a PurchaseOrder. Two separate FKs can fail here (vendorId -> * Vendor, receivedById -> Employee) — 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, extended for the * two-FK case. */ export async function createPurchaseOrder( input: CreatePurchaseOrderInput ): Promise { try { const created = await prisma.purchaseOrder.create({ data: { vendorId: input.vendorId, orderDate: input.orderDate, expectedDate: input.expectedDate, receivedAt: input.receivedAt ?? null, receivedById: input.receivedById ?? 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("received_by")) { return { ok: false, error: "receivedById does not reference an existing employee" }; } return { ok: false, error: "vendorId does not reference an existing vendor" }; } throw error; } } export async function listPurchaseOrders(): Promise { const rows = await prisma.purchaseOrder.findMany({ orderBy: [{ createdAt: "desc" }, { id: "desc" }], }); return rows.map(toRecord); }