Move business-context doc from workspace processes/ into repo

- docs/asset-license-management.md (moved from ../processes/, fixed
  relative links to stay accurate from the new location)
- CLAUDE.md now points at docs/asset-license-management.md
- removed stale no-git-repo-yet note, added git init/push status
This commit is contained in:
2026-07-30 18:32:01 +07:00
parent 19b4c96eaa
commit f2ca214b55
2 changed files with 108 additions and 5 deletions
+103
View File
@@ -0,0 +1,103 @@
# Quản lý Tài sản IT & License
## Trạng thái hiện tại
- Chưa có hệ thống tập trung — request xử lý rời rạc qua email/docx (vd `Firefly_License_Request.docx`, `Eric Pelatan.docx` ở gốc workspace `ubisoft/`, ngoài repo này). Không tracking được ai đang giữ gì, license nào sắp hết hạn/hết seat, tài sản nào sắp hết bảo hành.
- Đã chốt: kiến trúc monolith Next.js, không tách backend/frontend ở giai đoạn này (xem trigger để tách ở dưới).
- **Đã chốt: Postgres** (2026-07-29) — lý do: đảm bảo scale sau này + Power BI connect native, và bạn đã có kinh nghiệm dùng Postgres. Migrate task 1 (entity Request, state Draft) từ SQLite sang Postgres đã xong, CONFIRMED pass.
- **Đã chốt: Prisma (ORM) + Zod (validation)** (2026-07-30) — lý do Prisma over Drizzle cho solo dev: Prisma Studio (xem/sửa data qua GUI, khỏi viết psql query khi không có teammate hỏi), migration workflow tự động (`prisma migrate dev`), docs/community lớn hơn hẳn khi cần tự tra cứu một mình. Đánh đổi: nặng hơn Drizzle (query engine binary riêng, cần bước generate client), "magic" hơn raw SQL. Zod đi kèm để tận dụng type inference thay vì viết tay interface + hàm validate riêng.
- **Retrofit `Request` sang Prisma+Zod xong (2026-07-30)** — CONFIRMED qua test-runner độc lập: 6 test pass trên Postgres thật (qua `prisma migrate deploy`), type-check sạch, lint sạch. `type` chuyển từ `TEXT + CHECK` sang Postgres enum (`RequestType`) — tương đương semantics, không phải divergence. `id`/`createdAt` giữ nguyên hành vi cũ. Chi tiết đầy đủ ở `../CLAUDE.md`.
- Project scaffold (multi-agent dev workflow) đặt ngay tại repo này — xem `../CLAUDE.md`.
- File này trước đây nằm ở `ubisoft/processes/asset-license-management.md` (workspace chung, không phải git repo) — đã move vào đây (2026-07-30) vì nó gắn chặt với app này và cần update song song với code. `ubisoft/processes/README.md` vẫn giữ 1 dòng trỏ tới đây cho ai tìm từ đó.
## Mục tiêu
Theo dõi toàn bộ vòng đời tài sản/license: **Request → Approval → Procurement → Receiving → Delivery → Tracking → Thu hồi/Retire**, và trả lời nhanh được:
- Ai đang giữ máy/license nào?
- License nào sắp hết hạn hoặc hết seat?
- Tài sản nào sắp hết bảo hành?
- Chi phí/license theo phòng ban là bao nhiêu?
## Data model (sơ bộ)
| Entity | Ý nghĩa |
|---|---|
| `Employee` | id, tên, phòng ban, manager |
| `Vendor` | nhà cung cấp |
| `AssetItem` | hardware — serial number, model, ngày mua, bảo hành, trạng thái (in stock / assigned / repair / retired) |
| `License` | phần mềm — tên, vendor, tổng seat, seat đã dùng, ngày hết hạn, loại (perpetual/subscription) |
| `Request` | người yêu cầu, loại (asset/license), lý do, trạng thái |
| `PurchaseOrder` | gộp nhiều Request đã duyệt, vendor, ngày đặt, ngày dự kiến nhận |
| `Receipt` | nhận hàng — đối chiếu PO, ngày nhận, người nhận |
| `Assignment` | gán AssetItem/License cho Employee — ngày giao, ngày trả, biên bản ký nhận |
| `Request.servicenow_ticket` | field text, nhập tay số ticket ServiceNow liên quan (không tích hợp API ServiceNow ngay — volume thấp, nhập tay đủ dùng; để ngỏ cho auto-sync sau nếu cần) |
## Luồng trạng thái Request
```
Draft → Pending Approval → Approved → Ordered → Received → Delivered → Closed
↓
Rejected / Cancelled
```
## Module tương lai: Rack/Slot management + IoT (chưa build, chỉ note kiến trúc)
Mục tiêu xa hơn: biết chính xác asset nằm ở ô/slot nào trên tủ rack, và hướng dẫn người lấy đúng ô bằng đèn (pick-to-light).
**Data model thêm:**
- `Rack` (id, tên, vị trí — map với `rack-1/2/3` đã có ảnh ở `../../server-racks/`, thư mục ngoài repo này trong workspace `ubisoft/`)
- `RackSlot` (rack_id, vị trí U, `sensor_status` [empty/occupied — ghi bởi cảm biến vật lý], `assigned_asset_id` [nullable — ghi khi người dùng quét QR gán asset vào slot])
- Mismatch giữa `sensor_status` và `assigned_asset_id` (vd sensor báo occupied nhưng không có asset nào được assign) = tín hiệu cần kiểm tra thủ công (phát hiện thất thoát/quên cập nhật)
**Kiến trúc:** tách stack IoT riêng (broker MQTT hoặc tận dụng Home Assistant đã định dùng cho KNX ở văn phòng mới, xem `../../processes/new-office-fitout.md`) khỏi app chính — lý do là giao thức (MQTT/CoAP), data shape (time-series), và network/bảo mật (nên tách VLAN như camera/access control) đều khác hẳn CRUD web app. Tích hợp qua **1 bridge job** đọc/ghi trạng thái vào Postgres của app chính (`RackSlot.sensor_status`) — app chính không cần biết MQTT/HASS, Postgres vẫn là nguồn sự thật duy nhất.
**Luồng pick-to-light (tương lai):**
1. Request duyệt → nhận hàng → asset gán vào 1 `RackSlot` cụ thể → trạng thái Request thêm state mới "Ready for Pickup"
2. Người tới quét số ticket (ServiceNow, nhập tay ở trên) tại trạm quét QR gắn cạnh tủ rack → gọi API tra `RackSlot` tương ứng
3. Trạm quét = **consumer độc lập thứ 2** của app (ngoài web frontend) → endpoint này cần thiết kế như API thật (auth riêng cho thiết bị, không dựa session web) — đây là trigger hợp lệ đầu tiên để có 1 API boundary rõ ràng, dù chưa cần tách hẳn backend
4. App → bridge → publish lệnh bật đèn đúng slot qua IoT stack
5. Người lấy xong → xác nhận (quét lại hoặc sensor tự phát hiện chuyển empty) → tắt đèn, đóng Assignment/Request
QR gán asset vào slot (bước 1, thủ công hàng ngày) **không phải IoT** — chỉ là quét mã bằng camera trình duyệt/máy quét cầm tay, ghi thẳng qua API hiện có của app chính, không cần MQTT.
## Case đặc biệt cần xử lý
- Nhân viên nghỉ việc → checklist thu hồi asset + license (offboarding)
- License seat-based dùng chung (vd Adobe CC theo pool chung của phòng ban)
- Tài sản luân chuyển giữa phòng ban / chi nhánh (không phải "delivery" mới, mà là re-assign)
- Asset hỏng → gửi RMA/sửa chữa → cần state "in repair" tách khỏi "assigned"
## Quyết định kiến trúc
- Quy mô hiện tại <100 người, IT team nhỏ → **web app tự build gọn nhẹ, monolith** (Next.js full-stack, frontend + API routes chung 1 app) — self-hosted, tương tự pattern app "IT Knowledge Base + Ticket Analyzer" đã build cho `it-support.md` (workspace ngoài repo này), theo nguyên tắc chung của workspace là ưu tiên self-host hơn cloud (xem `../../processes/new-office-fitout.md`). **Không tách backend/frontend riêng ở giai đoạn này** — xem lý do & trigger để tách ở dưới.
- DB: **Postgres, đã chốt** (2026-07-29). Lý do: đảm bảo scale sau này, Power BI connect native/ổn định tới Postgres (còn SQLite cần ODBC driver gượng ép + không hợp để BI query đồng thời lúc app đang ghi), và đã có kinh nghiệm sẵn với Postgres. Cách lộ dữ liệu cho Power BI: tạo Postgres views riêng cho report (vd `v_requests_report`, `v_license_utilization`) + 1 read-only DB user trỏ Power BI vào, **không cần expose API riêng cho việc này**.
- Cron job (vd gửi email cảnh báo license sắp hết hạn hàng ngày) **không cần tách backend riêng** — làm 1 script độc lập (`scripts/send-license-expiry-emails.ts`) query thẳng Postgres, lịch chạy qua Windows Task Scheduler (đã có tiền lệ ở `xibo-signage.md`, workspace ngoài repo này) hoặc container cron riêng trong Docker. Cần chốt sau: SMTP nào, ngưỡng cảnh báo bao nhiêu ngày, gửi cho ai.
- Cân nhắc thay thế cho MVP: Airtable/Google Form nếu muốn test quy trình trước khi build — nhưng approval workflow nhiều bước + audit trail sẽ gượng ép nếu để lâu dài trên low-code.
- Reverse proxy khi deploy: **Nginx Proxy Manager** (đã chốt 2026-07-30) — tự host, có UI quản lý + tự động Let's Encrypt. Chưa dựng ngay vì app còn ở local dev, chưa tới bước deploy production. Khi deploy, app này khả năng sẽ dùng chung 1 NPM instance với các service self-host khác trong workspace (vd Xibo) thay vì mỗi app 1 reverse proxy riêng — cần xác nhận lại lúc thực sự deploy.
### Khi nào nên tách backend riêng (chưa áp dụng, chỉ ghi lại trigger)
1. Có consumer thứ 2 thật sự cần gọi API độc lập ngoài web frontend (vd trạm quét QR pick-to-light — xem module Rack/Slot ở dưới; hoặc tích hợp ServiceNow sau này)
2. Có job cần chạy tách biệt khỏi request-response của web (cron email đã quyết là chưa cần tách, chỉ cần script + scheduler)
3. Cần hạ tầng/bảo mật khác biệt (vd module IoT rack — xem dưới, lý do khác hẳn: giao thức, data shape, network)
4. Team lớn tới mức có người riêng phụ trách backend
"Muốn tích hợp với hệ thống khác" **tự nó không phải lý do để tách** — ngược lại, tích hợp chặt (vd Rack/Slot dùng chung AssetItem) là lý do để **gộp chung** app/DB thay vì làm hệ thống riêng rồi nối lại qua API.
## Process / SOP
*(Chưa có — sẽ bổ sung khi bắt đầu implement: quy trình tạo request mới, checklist nhận hàng, checklist bàn giao có ký nhận, checklist offboarding thu hồi thiết bị)*
## Việc còn dở
- Thiết kế ERD chi tiết + wireframe màn hình
- Entity còn lại: `Employee`, `AssetItem`, `License`, `PurchaseOrder`, `Receipt`, `Assignment`
- Migrate 2 request đang có (`Firefly_License_Request.docx`, `Eric Pelatan.docx`, ở workspace `ubisoft/` ngoài repo này) vào hệ thống mới sau khi có DB
- (Xa hơn, chưa ưu tiên) Rack/Slot + IoT pick-to-light, cron email cảnh báo license hết hạn, Power BI report views
## Ghi chú & bài học
- Migrate entity `Request` từ SQLite sang Postgres (2026-07-30) và setup ESLint đều đã **CONFIRMED xong** qua lượt verify độc lập (test-runner riêng, không phải agent vừa code tự chấm) — 6 test pass trên Postgres thật, lint sạch, type-check sạch. Chi tiết trạng thái nằm ở `../CLAUDE.md`, không duplicate ở đây.
---
Cập nhật lần cuối: 2026-07-30