AssetItem.model was a free-text string with no referential integrity.
Now a required FK to AssetModel, mirroring the Request.employeeId and
License.vendorId retrofit precedent (no backfill needed, no real
AssetItem data existed).
Nullable FK from License to PurchaseOrder for supply-side traceability
(which PO a license came in through), mirroring AssetItem.purchaseOrderId's
just-merged pattern exactly. Separate commit from the earlier
License.vendorId retrofit per the project's 1-commit-per-business-change
convention.
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.
- createRequestSchema now requires employee_id to be a UUID-format
string (was a free-text non-empty check), matching the new FK to
Employee in schema.prisma.
- createRequest() now returns a CreateRequestResult discriminated
union and catches Prisma P2003 (FK violation), returning a friendly
ok:false error instead of a raw 500 when employee_id does not
reference an existing Employee. POST /api/requests branches on
created.ok and returns 400 in that case.
- tests/requests.test.ts: every test that used to POST a fake string
employee_id (emp-1, emp-2) now creates a real Employee first via
createTestEmployee() and uses its id, since employee_id is now a
real FK. Added coverage for a syntactically invalid UUID and for a
well-formed but non-existent UUID (expects the friendly 400 from
the P2003 catch, not a raw 500) - the exact case this retrofit was
meant to protect against.
POST/GET /api/employees, same thin-route pattern as
src/app/api/requests/route.ts including the auth() 401 check
(middleware.ts excludes /api from page-route gating). POST also
surfaces createEmployee()'s FK-violation translation (nonexistent
managerId) as a 400, not a raw 500.
GET and POST both call auth() first and return 401 { error:
Unauthorized } when there is no session, before doing any
validation or DB work. This is the API-side half of route-gating -
middleware.ts handles page routes but deliberately excludes /api,
so API routes verify the session themselves.
Flat permission model only (per CLAUDE.md): this checks logged-in
or not, nothing more granular. No role/permission logic added.