feat: add License.purchaseOrderId (nullable FK)

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.
This commit is contained in:
toanhoang
2026-08-02 20:17:09 -04:00
parent fb52f473a5
commit abec58d239
5 changed files with 221 additions and 20 deletions
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "licenses" ADD COLUMN "purchase_order_id" UUID;
-- AddForeignKey
ALTER TABLE "licenses" ADD CONSTRAINT "licenses_purchase_order_id_fkey" FOREIGN KEY ("purchase_order_id") REFERENCES "purchase_orders"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+23 -12
View File
@@ -120,17 +120,25 @@ enum LicenseType {
// 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.
// subscription licenses usually have one. `purchaseOrderId` is a nullable FK
// to PurchaseOrder — see docs/asset-license-management.md's "## PurchaseOrder
// (gộp Receipt)" Traceability bullet: supply-side traceability ("which PO
// did this license come in through"). Nullable because not every License
// goes through a formal PO. Mirrors AssetItem.purchaseOrderId's just-merged
// pattern exactly — same nullable-FK shape, set at creation time only, no
// update endpoint for this field yet.
model License {
id String @id @default(uuid()) @db.Uuid
name 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
licenseType LicenseType @map("license_type")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
id String @id @default(uuid()) @db.Uuid
name 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
licenseType LicenseType @map("license_type")
purchaseOrderId String? @map("purchase_order_id") @db.Uuid
purchaseOrder PurchaseOrder? @relation(fields: [purchaseOrderId], references: [id])
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
@@map("licenses")
}
@@ -175,8 +183,10 @@ model Vendor {
// DB constraint, same approach as other cross-field invariants in this
// codebase (e.g. Assignment's assetItemId/licenseId XOR, not built yet).
// `assetItems` is the back-relation from the AssetItem.purchaseOrderId
// retrofit (supply-side traceability, same doc section). License/Request
// back-references are NOT part of this change — out of scope for this task.
// retrofit (supply-side traceability, same doc section). `licenses` is the
// back-relation from the License.purchaseOrderId retrofit (same doc
// section, same rationale). Request back-references are NOT part of this
// change — out of scope for this task.
model PurchaseOrder {
id String @id @default(uuid()) @db.Uuid
vendorId String @map("vendor_id") @db.Uuid
@@ -188,6 +198,7 @@ model PurchaseOrder {
receivedBy Employee? @relation(fields: [receivedById], references: [id])
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
assetItems AssetItem[]
licenses License[]
@@map("purchase_orders")
}