fix: retrofit License.vendor to vendorId FK + createLicense error handling
License.vendor (free-text string) is now vendorId, a required FK to
Vendor, per docs/asset-license-management.md's "## Vendor (chốt
2026-08-02)" spec. Mirrors the Request.employeeId string->FK precedent:
Zod validates UUID format only, Prisma's own FK constraint (P2003) is
caught at insert time and translated into a clean 400 instead of a raw
500. No License data existed yet, so this is a pure column-type
migration, no backfill needed.
Also converts createLicense() to the {ok,value}|{ok,error} result
pattern used elsewhere (createRequest, createAssetItem) instead of
returning a bare LicenseRecord promise - closes the gap flagged in
CLAUDE.md now that License has its first FK-violation error case to
handle.
This commit is contained in:
+48
-27
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "./prisma";
|
||||
|
||||
export type LicenseType = "perpetual" | "subscription";
|
||||
@@ -6,7 +7,7 @@ export type LicenseType = "perpetual" | "subscription";
|
||||
export interface LicenseRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
vendor: string;
|
||||
vendor_id: string;
|
||||
seats_total: number;
|
||||
seats_used: number;
|
||||
expiry_date: string | null;
|
||||
@@ -16,7 +17,7 @@ export interface LicenseRecord {
|
||||
|
||||
export interface CreateLicenseInput {
|
||||
name: string;
|
||||
vendor: string;
|
||||
vendorId: string;
|
||||
seatsTotal: number;
|
||||
expiryDate?: Date | null;
|
||||
licenseType: LicenseType;
|
||||
@@ -46,7 +47,14 @@ function dateString(message: string) {
|
||||
|
||||
const createLicenseSchema = z.object({
|
||||
name: nonEmptyString("name is required and must be a non-empty string"),
|
||||
vendor: nonEmptyString("vendor is required and must be a non-empty string"),
|
||||
// Now a real FK to Vendor (see schema.prisma) — only a UUID *format*
|
||||
// check here — whether it references a real vendor is left to Prisma's
|
||||
// own FK constraint at insert time (see createLicense()'s P2003 handling
|
||||
// below), rather than an extra pre-check query. Mirrors the same pattern
|
||||
// used for Request.employeeId in src/lib/requests.ts.
|
||||
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"),
|
||||
seatsTotal: z
|
||||
.number({ required_error: "seatsTotal is required and must be a positive integer", invalid_type_error: "seatsTotal is required and must be a positive integer" })
|
||||
.int({ message: "seatsTotal is required and must be a positive integer" })
|
||||
@@ -60,17 +68,17 @@ const createLicenseSchema = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates raw (untyped) license-creation input. name/vendor must be
|
||||
* non-empty strings, seatsTotal must be a positive integer, expiryDate is
|
||||
* optional/nullable but must be a valid date string if present, licenseType
|
||||
* must be one of the two enum values. seatsUsed is not accepted here — new
|
||||
* licenses always start at 0 (same pattern as AssetItem always starting
|
||||
* "in_stock"), seats are only ever consumed via other operations (once
|
||||
* Assignment exists), not at creation.
|
||||
* Validates raw (untyped) license-creation input. name must be a non-empty
|
||||
* string, vendorId must be a valid UUID string, seatsTotal must be a
|
||||
* positive integer, expiryDate is optional/nullable but must be a valid
|
||||
* date string if present, licenseType must be one of the two enum values.
|
||||
* seatsUsed is not accepted here — new licenses always start at 0 (same
|
||||
* pattern as AssetItem always starting "in_stock"), seats are only ever
|
||||
* consumed via other operations (once Assignment exists), not at creation.
|
||||
*/
|
||||
export function validateCreateLicenseInput(input: {
|
||||
name?: unknown;
|
||||
vendor?: unknown;
|
||||
vendorId?: unknown;
|
||||
seatsTotal?: unknown;
|
||||
expiryDate?: unknown;
|
||||
licenseType?: unknown;
|
||||
@@ -86,7 +94,7 @@ export function validateCreateLicenseInput(input: {
|
||||
function toRecord(row: {
|
||||
id: string;
|
||||
name: string;
|
||||
vendor: string;
|
||||
vendorId: string;
|
||||
seatsTotal: number;
|
||||
seatsUsed: number;
|
||||
expiryDate: Date | null;
|
||||
@@ -96,7 +104,7 @@ function toRecord(row: {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
vendor: row.vendor,
|
||||
vendor_id: row.vendorId,
|
||||
seats_total: row.seatsTotal,
|
||||
seats_used: row.seatsUsed,
|
||||
expiry_date: row.expiryDate ? row.expiryDate.toISOString() : null,
|
||||
@@ -105,22 +113,35 @@ function toRecord(row: {
|
||||
};
|
||||
}
|
||||
|
||||
export type CreateLicenseResult =
|
||||
| { ok: true; value: LicenseRecord }
|
||||
| { ok: false; error: string };
|
||||
|
||||
/**
|
||||
* Creates a License. No unique constraint exists on `name` (unlike
|
||||
* AssetItem.serialNumber), so unlike createAssetItem() there's no known
|
||||
* Prisma error code to catch/translate here.
|
||||
* Creates a License. If vendorId doesn't reference an existing Vendor,
|
||||
* 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/requests.ts's createRequest() pattern for employeeId.
|
||||
*/
|
||||
export async function createLicense(input: CreateLicenseInput): Promise<LicenseRecord> {
|
||||
const created = await prisma.license.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
vendor: input.vendor,
|
||||
seatsTotal: input.seatsTotal,
|
||||
expiryDate: input.expiryDate ?? null,
|
||||
licenseType: input.licenseType,
|
||||
},
|
||||
});
|
||||
return toRecord(created);
|
||||
export async function createLicense(input: CreateLicenseInput): Promise<CreateLicenseResult> {
|
||||
try {
|
||||
const created = await prisma.license.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
vendorId: input.vendorId,
|
||||
seatsTotal: input.seatsTotal,
|
||||
expiryDate: input.expiryDate ?? null,
|
||||
licenseType: input.licenseType,
|
||||
},
|
||||
});
|
||||
return { ok: true, value: toRecord(created) };
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") {
|
||||
return { ok: false, error: "vendorId does not reference an existing vendor" };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listLicenses(): Promise<LicenseRecord[]> {
|
||||
|
||||
Reference in New Issue
Block a user