Add AssetItem.cost/costCurrency/externalPoNumber, make import upsert-capable
CI / test (push) Failing after 9s

Three new fields: cost (Decimal, not Float -- exact money math),
costCurrency (free text), externalPoNumber (free text PO number from an
external system, distinct from purchaseOrderId which is a real FK into
this apps own PurchaseOrder table -- external exports PO numbers are
messy free text with nothing to link to here).

importAssetItems() now upserts: a row matching an existing assetTag or
serialNumber updates that row instead of erroring on the uniqueness
conflict. Needed to backfill cost/PO onto the 613 already-imported rows
without recreating them, and sets this up for future re-syncs when the
company re-exports their ITAM report. Update only touches fields the row
actually carries a value for (blank cell = no change, not clear) --
warrantyEndDate is the one field NOT auto-recomputed from warrantyMonths
on update (unlike create), so a manually-corrected date survives a
re-sync. Placeholder junk (Not available/Pending/N/A/etc, case
-insensitive) in PO number/cost/currency cells is treated as absent.

Re-ran the full 613-row import after this: 0 created, 613 updated, 0
errors -- backfilled cost onto the 8 rows that had one and PO number onto
the 2 that had a real (non-placeholder) value, verified against the
source file.

Hand-written migration, no Vitest coverage -- same deviation as the rest
of this branch, see CLAUDE.md. tsc/lint clean.
This commit is contained in:
toanhoang
2026-08-23 22:14:48 -04:00
parent 1360bb3c45
commit 2cbe746bde
6 changed files with 251 additions and 16 deletions
+6
View File
@@ -103,6 +103,12 @@ Backend CRUD xong cho toàn bộ entity chốt tới nay: `Request` (chỉ state
- Migration viết tay: `20260824081044_add_asset_item_asset_tag`, `20260824082411_asset_item_purchase_date_nullable`.
**Kết quả import thật:** 613/613 tạo thành công, 0 lỗi (khớp 100% với file gốc: 542 assigned + 71 in_stock). Trước khi import phải seed thủ công 14 `AssetCategory` mới + 135 `AssetModel` mới (script `seed-models.ts`, tự viết, chạy 1 lần qua tsx rồi xoá — không phải feature UI, vì import `AssetItem` cố tình KHÔNG tự tạo model, xem comment `importAssetItems()`). 6 asset thiếu ngày mua: `DANCONT014, DANCONT017, DANCONT018, DANCONT020, DANCONT021, DANCONT023` — cần bổ sung tay sau. **Phạm vi đợt này CHƯA làm:** Cost/Cost currency, PO number (text tự do, không map được vào `purchaseOrderId` UUID hiện tại), gán nhân viên thật (`User Assigned` → cần entity `Assignment` riêng, chưa đụng tới) — để đợt sau.
- **Backfill Employee + Assignment từ dữ liệu thật (2026-08-24):** tiếp theo đợt import 613 AssetItem — trích 95 Employee khác nhau (từ cột "User Assigned", 2 người không có Department trong file gốc do là "(Other)"/"(Freelance)" → gán tạm "Unknown") + 384 Assignment (asset ↔ nhân viên, dùng ngày "Assigned Date" thật của file làm `assignedAt` lịch sử). **Quan trọng: KHÔNG dùng `createAssignment()` có sẵn** — hàm đó cho luồng nghiệp vụ SỐNG (cấp phát mới từ kho, bắt buộc AssetItem đang `in_stock`, tự chuyển sang `assigned`, không cho set `assignedAt` tay). Asset import đã có sẵn `status=assigned` đúng thực tế rồi nên gọi hàm đó sẽ bị từ chối hết — đây là backfill lịch sử, phải ghi thẳng qua Prisma (`prisma.assignment.create()` trực tiếp, script `seed-assignments.ts`, chạy 1 lần qua tsx rồi xoá, không phải feature UI/lib mới). Kết quả: 95 employee + 384 assignment, 0 lỗi, khớp 100% (kiểm tra chéo DANLAP001 → Duc Luu Minh, đúng phòng ban + ngày gán). 158 asset `status=assigned` không có Assignment — đúng thực tế (thiết bị dùng chung, file gốc không gán riêng ai). **Chưa làm:** Cost, PO number, Job/Manager/Email của Employee (schema `Employee` hiện chỉ có name+department, chưa mở rộng thêm field từ file gốc).
- **`AssetItem.cost`/`costCurrency`/`externalPoNumber` + import upsert (2026-08-24):** thêm 3 field: `cost` (Decimal(14,2), không dùng Float để tránh sai số làm tròn), `costCurrency` (text tự do vd "VND"/"CNY"), `externalPoNumber` (text tự do — **khác** `purchaseOrderId` vốn là UUID FK thật vào bảng `PurchaseOrder`; PO number từ export ngoài thường là text rác không map được vào entity thật). CSV import/export cập nhật theo (`parseAssetItemsCsv`/`exportAssetItemsCsv`), cột `po number` tách biệt alias với `purchase order id` để không đụng nhau.
- **`importAssetItems()` giờ hỗ trợ upsert** (2026-08-24, thay đổi hành vi quan trọng): dòng có `assetTag`/`serialNumber` đã tồn tại sẽ **UPDATE** thay vì báo lỗi trùng như trước — cần thiết để backfill cost/PO lên 613 asset đã import trước đó, và hữu ích cho lần công ty xuất file ITAM mới sau này (re-sync định kỳ, không phải one-off). Update chỉ đụng field nào dòng CSV **có giá trị** (ô trống = không đổi, không phải xoá) — riêng `warrantyEndDate` chỉ update khi dòng có giá trị rõ ràng, KHÔNG tự tính lại từ `warrantyMonths` như nhánh CREATE (tránh ghi đè warranty đã sửa tay). Kết quả trả về thêm field `updated` bên cạnh `created`/`errors`.
- Dữ liệu rác dạng "Not available"/"Pending"/"N/A"/"None"/"-" (case-insensitive) ở cột PO number/cost/cost currency được coi là rỗng (hàm `cleanImportText()`/`parseImportCost()`), không lưu literal text rác vào DB.
Migration viết tay: `20260824090149_add_asset_item_cost_po`.
**Kết quả backfill thật:** chạy lại đúng file CSV 613 dòng (giờ có thêm 3 cột) → **0 tạo mới, 613 update, 0 lỗi** (đúng — không tạo trùng). 8/613 asset có cost thật (khớp 100% giá trị gốc, kiểm tra chéo), 2/613 có PO number thật, còn lại đúng là rác đã lọc.
**Việc còn dở, ý tưởng đã thống nhất với user (chưa làm):** tách `Department` thành entity riêng (như `Area`/`Vendor`), cho cả `Employee` và `AssetItem` cùng tham chiếu qua FK thay vì mỗi bên giữ 1 chuỗi text tự do — giúp asset dùng chung (không gán ai) vẫn gắn được department, và tránh lệch chính tả giữa 2 bảng.
- **CI (2026-08-20):** Gitea Actions, workflow `.gitea/workflows/ci.yml` — chạy `npm ci` + `npx prisma generate` + `npx tsc --noEmit` + `npm run lint` + `npm test` (Postgres service container) trên mọi push/PR. Cần runner tự host (`act_runner`) đã đăng ký với instance Gitea `git.toanhoang.work` — xem chi tiết setup ở `/opt/homelab/gitea/CLAUDE.md` (ngoài repo này) nếu cần thêm runner. **Chưa có auto-deploy** — deploy production vẫn làm tay trên `appsrv01`: `docker compose up -d --build app` rồi `docker exec asset-license-mgmt-app npm run db:migrate` (thứ tự bắt buộc: build trước, migrate sau khi container mới đã chạy).
## Việc còn dở