Files
ubi-dan-itam/src/lib/requests.ts
T

155 lines
5.3 KiB
TypeScript

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<CreateRequestResult> {
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<RequestRecord[]> {
const rows = await prisma.request.findMany({
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
});
return rows.map(toRecord);
}