feat: add Request.purchaseOrderId (nullable FK)

This commit is contained in:
toanhoang
2026-08-02 20:24:59 -04:00
parent 3dd1b22828
commit 65232fc62c
5 changed files with 158 additions and 18 deletions
+29 -5
View File
@@ -16,6 +16,7 @@ export interface RequestRecord {
type: RequestType;
reason: string;
status: RequestStatus;
purchase_order_id: string | null;
created_at: string;
}
@@ -23,6 +24,7 @@ export interface CreateRequestInput {
employee_id: string;
type: RequestType;
reason: string;
purchase_order_id?: string | null;
}
export type ValidationResult =
@@ -55,6 +57,18 @@ const createRequestSchema = z.object({
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(),
});
/**
@@ -66,6 +80,7 @@ export function validateCreateRequestInput(input: {
employee_id?: unknown;
type?: unknown;
reason?: unknown;
purchase_order_id?: unknown;
}): ValidationResult {
const result = createRequestSchema.safeParse(input);
if (!result.success) {
@@ -81,6 +96,7 @@ function toRecord(row: {
type: RequestType;
reason: string;
status: string;
purchaseOrderId: string | null;
createdAt: Date;
}): RequestRecord {
return {
@@ -89,6 +105,7 @@ function toRecord(row: {
type: row.type,
reason: row.reason,
status: row.status as RequestStatus,
purchase_order_id: row.purchaseOrderId,
created_at: row.createdAt.toISOString(),
};
}
@@ -98,11 +115,13 @@ export type CreateRequestResult =
| { ok: false; error: string };
/**
* Creates a Request. If employee_id doesn't reference an existing Employee,
* Prisma raises a known error (code P2003, FK violation) — caught here and
* translated into the same `{ok:false,error}` shape as input validation
* failures, rather than letting a raw Prisma error leak to the API layer.
* Mirrors src/lib/employees.ts's createEmployee() pattern for managerId.
* 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 {
@@ -111,11 +130,16 @@ export async function createRequest(input: CreateRequestInput): Promise<CreateRe
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;