Lets the operator upload a CSV of already-known assets (asset tag, model,
serial, old area/assignee) before an office-move walk, via a new
session-authed /inventory-scans/import page. The Telegram bot gains a
"tim trong list" search step at the asset-tag prompt: picking a match
pre-fills tag/serial and offers one-tap "giu nguyen" buttons for the
old area/assignee, then marks that row matched once the scan is created.
Follows the same-day decision to turn off local OCR (qwen2.5vl via Ollama)
for this feature -- see CLAUDE.md and the ITAM/Ollama vault notes for the
accuracy/speed numbers behind that call.
Deviates from repo convention same as the InventoryScan work above: no
Vitest coverage (manual smoke test only, via simulated webhook calls),
hand-written migration, not through the ba->pm->feature-implementer->
test-runner->code-reviewer pipeline. tsc/lint are clean.
Office-move inventory kiem ke feature. See docs/asset-license-management.md InventoryScan section for full business rationale (Area vs Seat, why not AssetItem, asset tag/serial nullable).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Was implicitly SET NULL (Prisma default for nullable FK columns),
inconsistent with employeeId's RESTRICT. Once a delete endpoint for
AssetItem/License exists, SET NULL would silently null out historical
Assignment rows on delete, erasing what was assigned and re-violating
the assetItemId/licenseId XOR retroactively. No delete endpoint exists
yet, so no real data was ever affected by the old behavior.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
New entity tracking repair/warranty/preventive maintenance jobs for an
AssetItem. Reuses Vendor (nullable) for the repair location, no cost
field (mirrors PurchaseOrder). "At most 1 open record per AssetItem"
invariant left to the application layer, mirroring Assignment.
Links Employee to exactly one of AssetItem/License (XOR enforced at the
Zod layer, not built yet) via nullable FKs, plus assignedAt/returnedAt/
signedAt timestamps. See docs/asset-license-management.md's "Assignment"
section for the full spec.
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).
Per-category dynamic attribute definitions (key/label/dataType/required/
sortOrder), FK to AssetCategory, composite unique (categoryId, key) so the
same key is allowed across different categories but not duplicated within
one. dataType is a new closed Prisma enum (AssetAttributeDataType), a
dev-defined technical set distinct from AssetCategoryValueGroup.
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.
Retrofits Request.employeeId from a free-text String with no
referential integrity to a real relation: employeeId String @db.Uuid
+ employee Employee @relation(...), plus the inverse Employee.requests
back-relation field. Migration alters requests.employee_id from TEXT
to UUID with an FK constraint (drop+recreate, confirmed empty table -
no production/dev data existed to migrate).
Employee directory entity per docs/asset-license-management.md's data
model table (id, name, department, manager). department stays a plain
String for now (not a separate Department entity). managerId is a
nullable self-relation (EmployeeManager) - top-level employees have
managerId = null.