Add ExpectedAsset CSV import + bot search/pick for pre-known inventory
CI / test (push) Failing after 8s

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.
This commit is contained in:
toanhoang
2026-08-21 08:59:18 -04:00
parent b4e2f6c83e
commit 8ac95c974b
10 changed files with 480 additions and 17 deletions
+34
View File
@@ -565,6 +565,40 @@ model InventoryScan {
telegramChatId String? @map("telegram_chat_id")
telegramMessageId Int? @map("telegram_message_id")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
// Back-reference for ExpectedAsset.matchedScanId -- set once someone
// picks this scan's pre-uploaded row via the bot's "🔍 Tìm trong list"
// step. No @relation attribute needed here; the FK lives on the other
// side.
expectedAsset ExpectedAsset?
@@map("inventory_scans")
}
// A row from a pre-move Excel/CSV export of known assets (asset tag,
// model, old area/owner), uploaded via POST /api/expected-assets/import
// before an office-move walk starts (2026-08-21 decision -- team decided
// plain manual entry was faster/more reliable than local OCR for ~300-400
// assets, see docs/asset-license-management.md, but typing in a KNOWN
// asset tag from scratch during the walk is still wasted effort when it's
// sitting right here). Purely a lookup/staging table for the bot's "🔍 Tìm
// trong list" picker -- modelText/oldAreaText/oldAssigneeText are free
// text straight from the import file, deliberately NOT FK'd into
// AssetModel/Area/Employee: import-file text won't reliably match this
// app's catalog IDs/spelling, and forcing that match at import time would
// block the whole import on one typo instead of just being a soft
// "here's the old value" suggestion during the walk. Marked matched (via
// matchedScanId) once someone actually picks it while creating a scan, so
// it stops showing up in future searches.
model ExpectedAsset {
id String @id @default(uuid()) @db.Uuid
assetTag String? @map("asset_tag")
modelText String @map("model_text")
serialNumber String? @map("serial_number")
oldAreaText String? @map("old_area_text")
oldAssigneeText String? @map("old_assignee_text")
matchedScanId String? @unique @map("matched_scan_id") @db.Uuid
matchedScan InventoryScan? @relation(fields: [matchedScanId], references: [id])
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
@@map("expected_assets")
}