// Prisma schema — source of truth for the DB schema going forward, // replacing the hand-written `CREATE TABLE IF NOT EXISTS` migration that // used to live in src/lib/db.ts. Only the Request entity (Draft state only) // exists so far — see ../processes/asset-license-management.md for the full // data model / state machine this will grow into. generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // Fixed, closed set of 2 values tied to the domain (asset vs license // request) — a native Postgres enum is the idiomatic Prisma mapping here. // This replaces the old `type TEXT NOT NULL CHECK (type IN ('asset', // 'license'))` column with `type "RequestType" NOT NULL` backed by a // `CREATE TYPE "RequestType" AS ENUM ('asset', 'license')`. Same validation // guarantee (only these two values are ever storable), different mechanism. // See feature-implementer report for the enum-vs-TEXT+CHECK trade-off. enum RequestType { asset license } model Request { id String @id @default(uuid()) @db.Uuid // Was a free-text String with no referential integrity — retrofitted to a // real FK once Employee records existed to point to (see Employee model // above). No production data existed at the time of this change (`requests` // table was empty), so this was a plain column-type migration, not a // backfill. employeeId String @map("employee_id") @db.Uuid employee Employee @relation(fields: [employeeId], references: [id]) type RequestType reason String // Only the 'Draft' state is implemented so far. Deliberately kept as a // plain string (not an enum) until more states exist — see // src/lib/requests.ts's RequestStatus type. status String @default("Draft") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) @@map("requests") } // Employee directory — see docs/asset-license-management.md's "Data model // (sơ bộ)" table (id, tên, phòng ban, manager). `department` is deliberately // a plain String for now, not a separate Department entity — that's a // possible future normalization, not built yet. `managerId` is a nullable // self-relation: top-level employees (no manager) have `managerId = null`. model Employee { id String @id @default(uuid()) @db.Uuid name String department String managerId String? @map("manager_id") @db.Uuid manager Employee? @relation("EmployeeManager", fields: [managerId], references: [id]) reports Employee[] @relation("EmployeeManager") requests Request[] createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) @@map("employees") } // Fixed, closed set of 4 values tied to the domain (see docs/ // asset-license-management.md's "Data model (sơ bộ)" table) — native // Postgres enum, same rationale as RequestType above. enum AssetItemStatus { in_stock assigned repair retired } // Hardware inventory — see docs/asset-license-management.md's "Data model // (sơ bộ)" table (serial number, model, ngày mua, bảo hành, trạng thái). // No Vendor/Assignment relation yet — those entities don't exist yet // (see "Việc còn dở" in CLAUDE.md), so this is a standalone entity for now, // same starting shape as Employee before Request grew an FK to it. // warrantyEndDate is nullable: not every asset has known warranty terms at // intake time (smallest sensible assumption — doesn't block creation). model AssetItem { id String @id @default(uuid()) @db.Uuid serialNumber String @unique @map("serial_number") model String purchaseDate DateTime @map("purchase_date") @db.Date warrantyEndDate DateTime? @map("warranty_end_date") @db.Date status AssetItemStatus @default(in_stock) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) @@map("asset_items") } // Fixed, closed set of 2 values tied to the domain (see docs/ // asset-license-management.md's "Data model (sơ bộ)" table) — native // Postgres enum, same rationale as RequestType/AssetItemStatus above. enum LicenseType { perpetual subscription } // 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). `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 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) @@map("licenses") } // Purchasing counterparty — see docs/asset-license-management.md's "## Vendor // (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. `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 name String contactEmail String @map("contact_email") contactPhone String @map("contact_phone") address String taxCode String? @map("tax_code") notes String? createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) licenses License[] @@map("vendors") } // Minimal internal auth accounts — NOT company SSO (deliberately ruled out, // see CLAUDE.md / auth decision notes). A handful of trusted IT staff // accounts, created by hand via scripts/create-user.ts, no self-registration. // Auth.js v5 Credentials provider + JWT session strategy is used, so this is // intentionally just a plain user table — no `Account`/`Session`/ // `VerificationToken` models, those exist for OAuth account linking / DB // session strategy, neither of which applies here. No `role` field either: // permission model is flat for now (any logged-in user can do anything). model User { id String @id @default(uuid()) @db.Uuid email String @unique passwordHash String @map("password_hash") name String createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) @@map("users") }