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:
toanhoang
2026-08-02 19:41:33 -04:00
parent 9a4a3fede3
commit 9d99ebb425
5 changed files with 224 additions and 87 deletions
@@ -0,0 +1,13 @@
/*
Warnings:
- You are about to drop the column `vendor` on the `licenses` table. All the data in the column will be lost.
- Added the required column `vendor_id` to the `licenses` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "licenses" DROP COLUMN "vendor",
ADD COLUMN "vendor_id" UUID NOT NULL;
-- AddForeignKey
ALTER TABLE "licenses" ADD CONSTRAINT "licenses_vendor_id_fkey" FOREIGN KEY ("vendor_id") REFERENCES "vendors"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+22 -18
View File
@@ -102,17 +102,20 @@ enum LicenseType {
// Software license inventory — see docs/asset-license-management.md's "Data
// model (sơ bộ)" table (tên, vendor, tổng seat, seat đã dùng, ngày hết hạn,
// loại). `vendor` is deliberately a plain String for now, not a separate
// Vendor entity — no Vendor entity exists yet, same "plain string for now"
// precedent as Employee.department. `seatsUsed` is a stored counter
// (defaults to 0 at creation), not a computed value — it will only ever be
// set to 0 here since Assignment (the thing that would consume seats)
// doesn't exist yet. `expiryDate` is nullable: perpetual licenses never
// expire, only subscription licenses usually have one.
// loại). `vendorId` was a free-text `vendor` String until the "## Vendor
// (chốt 2026-08-02)" retrofit — same precedent as Request.employeeId
// (string -> FK once Employee existed). No production `License` data
// existed at the time of this change, so this was a plain column-type
// migration, not a backfill. `seatsUsed` is a stored counter (defaults to 0
// at creation), not a computed value — it will only ever be set to 0 here
// since Assignment (the thing that would consume seats) doesn't exist yet.
// `expiryDate` is nullable: perpetual licenses never expire, only
// subscription licenses usually have one.
model License {
id String @id @default(uuid()) @db.Uuid
name String
vendor String
vendorId String @map("vendor_id") @db.Uuid
vendor Vendor @relation(fields: [vendorId], references: [id])
seatsTotal Int @map("seats_total")
seatsUsed Int @default(0) @map("seats_used")
expiryDate DateTime? @map("expiry_date") @db.Date
@@ -126,20 +129,21 @@ model License {
// (chốt 2026-08-02)". Deliberately independent of AssetModel.manufacturer
// (OEM, e.g. "Dell") — Vendor is the reseller/distributor actually bought
// from (e.g. "FPT Trading"), a different concept that isn't always the same
// company. No FKs on this entity yet: it's the starting point for later
// FKs from PurchaseOrder/License (License.vendor string -> License.vendorId
// FK retrofit is noted in the doc as a separate follow-up, not done here).
// taxCode/notes are nullable — not every vendor record has a known VN tax
// code or extra notes at creation time.
// company. `licenses` is the back-relation from the License.vendor ->
// License.vendorId retrofit (same doc section) — PurchaseOrder is a future
// FK from this entity, not built yet. taxCode/notes are nullable — not
// every vendor record has a known VN tax code or extra notes at creation
// time.
model Vendor {
id String @id @default(uuid()) @db.Uuid
id String @id @default(uuid()) @db.Uuid
name String
contactEmail String @map("contact_email")
contactPhone String @map("contact_phone")
contactEmail String @map("contact_email")
contactPhone String @map("contact_phone")
address String
taxCode String? @map("tax_code")
taxCode String? @map("tax_code")
notes String?
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
licenses License[]
@@map("vendors")
}