feat: add AssetItem.purchaseOrderId (nullable FK)

This commit is contained in:
toanhoang
2026-08-02 20:09:06 -04:00
parent 4d12885500
commit a6d7c7b302
5 changed files with 155 additions and 20 deletions
+25 -1
View File
@@ -11,6 +11,7 @@ export interface AssetItemRecord {
purchase_date: string;
warranty_end_date: string | null;
status: AssetItemStatus;
purchase_order_id: string | null;
created_at: string;
}
@@ -19,6 +20,7 @@ export interface CreateAssetItemInput {
model: string;
purchaseDate: Date;
warrantyEndDate?: Date;
purchaseOrderId?: string | null;
}
export type ValidationResult =
@@ -48,12 +50,24 @@ const createAssetItemSchema = z.object({
model: nonEmptyString("model is required and must be a non-empty string"),
purchaseDate: dateString("purchaseDate is required and must be a valid date string"),
warrantyEndDate: dateString("warrantyEndDate must be a valid date string").optional(),
// FK to PurchaseOrder — nullable/optional, supply-side traceability (not
// every AssetItem goes through a formal PO). Only a UUID *format* check
// here, whether it references a real PurchaseOrder is left to Prisma's
// own FK constraint at insert time (see createAssetItem()'s P2003
// handling below). Mirrors src/lib/purchase-orders.ts's receivedById
// optional().nullable() pattern.
purchaseOrderId: z
.string({ invalid_type_error: "purchaseOrderId must be a valid UUID string" })
.uuid("purchaseOrderId must be a valid UUID string")
.optional()
.nullable(),
});
/**
* Validates raw (untyped) asset-item-creation input. serialNumber/model
* must be non-empty strings, purchaseDate must be a valid date string,
* warrantyEndDate is optional but must be a valid date string if present.
* purchaseOrderId is optional/nullable but must be a valid UUID if present.
* status is not accepted here — new assets always start "in_stock" (same
* pattern as Request always starting "Draft"), later states are set via
* other operations, not at creation.
@@ -63,6 +77,7 @@ export function validateCreateAssetItemInput(input: {
model?: unknown;
purchaseDate?: unknown;
warrantyEndDate?: unknown;
purchaseOrderId?: unknown;
}): ValidationResult {
const result = createAssetItemSchema.safeParse(input);
if (!result.success) {
@@ -79,6 +94,7 @@ function toRecord(row: {
purchaseDate: Date;
warrantyEndDate: Date | null;
status: AssetItemStatus;
purchaseOrderId: string | null;
createdAt: Date;
}): AssetItemRecord {
return {
@@ -88,6 +104,7 @@ function toRecord(row: {
purchase_date: row.purchaseDate.toISOString(),
warranty_end_date: row.warrantyEndDate ? row.warrantyEndDate.toISOString() : null,
status: row.status,
purchase_order_id: row.purchaseOrderId,
created_at: row.createdAt.toISOString(),
};
}
@@ -102,7 +119,10 @@ export type CreateAssetItemResult =
* 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 createEmployee()/createRequest()'s P2003
* handling for FK violations.
* handling for FK violations. If purchaseOrderId doesn't reference an
* existing PurchaseOrder, Prisma raises P2003 (FK constraint violation) —
* caught the same way, mirrors src/lib/purchase-orders.ts's
* createPurchaseOrder() P2003 handling.
*/
export async function createAssetItem(
input: CreateAssetItemInput
@@ -114,6 +134,7 @@ export async function createAssetItem(
model: input.model,
purchaseDate: input.purchaseDate,
warrantyEndDate: input.warrantyEndDate ?? null,
purchaseOrderId: input.purchaseOrderId ?? null,
},
});
return { ok: true, value: toRecord(created) };
@@ -121,6 +142,9 @@ export async function createAssetItem(
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
return { ok: false, error: "serialNumber already exists" };
}
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") {
return { ok: false, error: "purchaseOrderId does not reference an existing purchase order" };
}
throw error;
}
}